---
name: react-three-webgl
description: "React Three Fiber and drei implementation reference: scenes, cameras, lighting, GLTF loading, scroll-linked 3D, post-processing, performance budgets, fallbacks. Use when writing or reviewing R3F/three.js code for a hero object, configurator, 3D data viz, or ambient background. Not for the end-to-end 3D marketing site workflow: use build-3d-website. Not for playable games with mechanics or saves: use build-3d-game."
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: rewritten from patterns in pmndrs react-three-fiber and drei examples (MIT)
  category: 3D & Games
---

# React Three / WebGL

R3F renders a Three.js scene from a React component tree; drei supplies the helpers that kill the boilerplate. 3D is the heaviest thing on a page, so the rule is one purposeful 3D moment, lazy-loaded, with a real fallback. If CSS, SVG, or a 2D animation delivers the same moment, use that instead.

## Minimal scene

```bash
npm install three @react-three/fiber @react-three/drei
```

```tsx
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment, Float } from "@react-three/drei";

export function Hero3D() {
  return (
    <Canvas camera={{ position: [0, 0, 5], fov: 45 }} dpr={[1, 2]}>
      <ambientLight intensity={0.4} />
      <directionalLight position={[5, 5, 5]} intensity={1} />
      <Float speed={1.5} rotationIntensity={0.6} floatIntensity={0.8}>
        <mesh>
          <icosahedronGeometry args={[1, 0]} />
          <meshStandardMaterial color="#6366f1" roughness={0.3} metalness={0.6} />
        </mesh>
      </Float>
      <Environment preset="city" />
      <OrbitControls enablePan={false} enableZoom={false} />
    </Canvas>
  );
}
```

`dpr={[1, 2]}` caps pixel ratio so retina phones do not render 4x the pixels. Inside `<Canvas>` is Three.js space, outside is normal DOM.

## Workflow

1. **Justify the 3D.** Name the one moment it serves. Rule out a 2D alternative first.
2. **Mount client-only.** `next/dynamic({ ssr: false })` or `React.lazy` around the Canvas. Never SSR WebGL.
3. **Build the scene** with drei helpers instead of raw Three.js setup. Read references/drei-catalog.md when picking helpers for controls, loading, motion, scroll, text, or perf.
4. **Load models correctly.** Compressed `.glb` (Draco or Meshopt), a few MB max. `useGLTF` inside `<Suspense fallback={...}>`, plus `useGLTF.preload(url)`.
5. **Animate in `useFrame`, mutate refs.** `ref.current.rotation.y += delta`. Never call `setState` per frame. For scroll: `ScrollControls` + `useScroll`, drive the camera from `scroll.offset`.
6. **Apply the perf budget.** Cap `dpr`, add `AdaptiveDpr` or `PerformanceMonitor`, instance repeats, `BakeShadows` for static lights, dispose anything you created manually.
7. **Add the fallback layer.** The page must convey its message with the canvas removed: static image or text behind it. Respect `prefers-reduced-motion` by freezing auto-rotation and idle float. Decorative canvases get `aria-hidden`; interactive controls get labeled DOM affordances. Essential content never lives only inside WebGL.

## Good vs Bad

**Bad:** `useFrame(() => setRotation(r => r + 0.01))` driving a prop into the mesh. React re-renders 60 times a second, jank on any real page.

**Good:** `useFrame((_, delta) => { meshRef.current.rotation.y += delta; })`. Direct mutation, zero re-renders, frame-rate independent because it uses `delta`.

## Verification

Run the site in Chrome, open DevTools Performance, record 5 seconds of the 3D section with 4x CPU throttling. Expect a steady frame rate near 60fps and no long tasks from React renders. If frames drop: check for `setState` in `useFrame`, uncapped `dpr`, or un-instanced repeated meshes.

Then disable JavaScript (or block WebGL) and reload. Expect the page to still communicate its message via the fallback. If the section is blank, the fallback is missing: add it before shipping.

Check payload: `ls -lh public/models/`. Expect hero models in the low single-digit MB. Bigger: recompress with Draco/Meshopt or reduce the mesh.

## Footguns

- **SSR-rendered Canvas.** Crashes or hydration mismatch, `window is not defined`. Fix: dynamic import with `ssr: false`.
- **setState inside useFrame.** Re-render storm. Fix: mutate refs, keep React state for discrete UI events only.
- **Uncompressed GLTF.** Tens of MB blocking interaction. Fix: export `.glb` with Draco compression, preload, wrap in Suspense.
- **Multiple SPF-style stacking of post effects.** Each postprocessing pass costs a full-screen render. Fix: one effect (Bloom or DoF), not a stack.
- **Leaked resources on unmount.** Manually created geometries/materials/textures persist. Fix: R3F auto-disposes declarative objects; call `.dispose()` on anything created imperatively.

## Completion checklist

- [ ] One purposeful 3D moment; 2D alternative was ruled out
- [ ] Canvas is client-only and lazy-mounted
- [ ] `dpr` capped; instancing/adaptive perf where relevant
- [ ] Models are compressed `.glb` under Suspense with preload
- [ ] No `setState` in `useFrame`; refs mutated with `delta`
- [ ] DOM fallback carries the message; reduced-motion respected; decorative canvas `aria-hidden`
- [ ] Profiled near 60fps under CPU throttling

Any box unchecked: not done. Fix or say so.
