React Three Fiber (R3F) performance and pattern reference. Use when writing, reviewing, or optimising a React Three Fiber application — covers useFrame animation, preventing re-renders, Zustand selectors, Drei helpers, Suspense/loading, visibility toggling, component patterns, physics (Rapier), and post-processing. Adapted from emalorenzo/three-agent-skills (MIT).
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
React Three Fiber (R3F) performance and pattern reference. Use when writing, reviewing, or optimising a React Three Fiber application — covers useFrame animation, preventing re-renders, Zustand selectors, Drei helpers, Suspense/loading, visibility toggling, component patterns, physics (Rapier), and post-processing. Adapted from emalorenzo/three-agent-skills (MIT).
license
MIT
compatibility
Portable reference skill for agents that support markdown skills or prompt files. Works best alongside React Three Fiber project source files and browser profiler captures.
disable-model-invocation
true
metadata
{"owner":"game-delivery","version":"2.0.0","language":"en-GB","category":"web-rendering","upstream_references":["https://github.com/emalorenzo/three-agent-skills (MIT — see NOTICE.md)"],"tags":["react-three-fiber","r3f","drei","zustand","webgl","performance","animation","suspense","rapier"],"intents":["code-review","re-render-audit","useframe-review","state-selector-review","suspense-review","physics-setup"],"output_types":["rule-citation","review-findings","optimisation-plan","code-correction"]}
React Three Fiber Best Practices
Reference guide for @react-three/fiber v8+. Rules are grouped by impact. Within each category, critical rules are marked CRITICAL.
1. Animation and useFrame
CRITICAL — Never call setState inside useFrame
This is the single most common performance killer in R3F.useFrame runs at 60 fps. Calling setState inside it triggers 60 React re-renders per second, causing CPU spikes, garbage collection pauses, and frame drops.
// BAD — 60 re-renders per secondfunctionBadMesh() {
const [rotation, setRotation] = useState(0);
useFrame((state, delta) => {
setRotation(r => r + delta); // triggers re-render every frame
});
return<meshrotation-y={rotation} />;
}
// GOOD — mutates the Three.js object directly, zero re-rendersfunctionGoodMesh() {
const meshRef = useRef();
useFrame((state, delta) => {
meshRef.current.rotation.y += delta; // direct mutation, no React cycle
});
return<meshref={meshRef} />;
}
Use delta time for frame-rate-independent animation
CRITICAL — Always use the delta argument. Without it, animations run at different speeds on different devices.
functionAnimatedMesh() {
const meshRef = useRef();
useFrame(({ clock }, delta) => {
// GOOD — delta gives frame-rate independence
meshRef.current.rotation.y += 1.0 * delta; // 1 radian/second// GOOD — elapsedTime for oscillation at fixed frequency
meshRef.current.position.y = Math.sin(clock.elapsedTime * 2) * 0.5;
// GOOD — frame-rate-independent lerp
meshRef.current.position.lerp(
targetPosition,
1 - Math.pow(0.001, delta)
);
});
return<meshref={meshRef} />;
}
Read global state without subscribing in useFrame
functionAnimatedByStore() {
const meshRef = useRef();
useFrame(() => {
// getState() does NOT subscribe — no re-rendersconst { targetPosition, speed } = useGameStore.getState();
meshRef.current.position.lerp(targetPosition, speed);
});
return<meshref={meshRef} />;
}
Prioritise useFrame calls
// Lower priority number = runs first (default is 0)useFrame(() => { /* physics update */ }, -1);
useFrame(() => { /* camera follows player */ }, 0);
useFrame(() => { /* UI overlay */ }, 1);
2. State Management with Zustand
CRITICAL — Use selectors, not the whole store
// BAD — re-renders on ANY store changefunctionBadComponent() {
const store = useGameStore(); // subscribes to the entire storereturn<meshposition-x={store.playerX} />;
}
// GOOD — re-renders only when playerX changesfunctionGoodComponent() {
const playerX = useGameStore(state => state.playerX);
return<meshposition-x={playerX} />;
}
Select multiple values with shallow
import { shallow } from'zustand/shallow';
functionPositionComponent() {
const { x, y, z } = useGameStore(
state => ({ x: state.x, y: state.y, z: state.z }),
shallow // prevents re-render if the selected values are reference-equal
);
return<meshposition={[x,y, z]} />;
}
Transient subscriptions — subscribe without re-renders
functionTransientComponent() {
const meshRef = useRef();
useEffect(() => {
const unsubscribe = useGameStore.subscribe(
state => state.playerPosition,
position => {
// Direct mutation — no React re-render
meshRef.current?.position.copy(position);
}
);
return unsubscribe;
}, []);
return<meshref={meshRef} />;
}
Zustand selector performance summary
Method
Re-renders
Use when
useStore()
Every change
Never
useStore(s => s.value)
When value changes
Standard UI or 3D props
useStore(s => ({...}), shallow)
When any selected value changes
Multiple values
useStore.subscribe()
Never
Continuous position/rotation updates
useStore.getState()
Never
Inside useFrame
3. Visibility and Mounting
Toggle visibility instead of remounting for frequently hidden objects
Mounting and unmounting Three.js objects is expensive: it triggers disposal, geometry re-upload, shader recompilation.
// BAD for frequent show/hide — disposes and recreates geometry every togglefunctionBadToggle({ show }) {
return show ? <mesh><boxGeometry /><meshStandardMaterial /></mesh> : null;
}
// GOOD — object stays in GPU memory, just skipped in renderfunctionGoodToggle({ show }) {
const meshRef = useRef();
useEffect(() => {
if (meshRef.current) meshRef.current.visible = show;
}, [show]);
return<meshref={meshRef}><boxGeometry /><meshStandardMaterial /></mesh>;
}
// GOOD — declarative propfunctionGoodToggleDeclarative({ show }) {
return<meshvisible={show}><boxGeometry /><meshStandardMaterial /></mesh>;
}
Reserve conditional mounting for objects that are rarely needed and where memory matters more than GPU state (e.g., off-screen zones loaded on demand).
functionTree({ position }) {
const { scene } = useGLTF('/assets/tree.glb');
return<primitiveobject={scene.clone()}position={position} />;
}
5. Component Patterns
Separate animated components from UI-driven components
Animation components should avoid React state entirely. UI-driven components (health bars, menus) can use state normally — they are not in the render loop.
// Animation — use refs and useFrame onlyfunctionPlayerMesh({ playerRef }) {
useFrame(() => {
playerRef.current.position.x = useGameStore.getState().playerX;
});
return<meshref={playerRef}><capsuleGeometry /></mesh>;
}
// UI — can use state freely (not in render loop)functionHealthBar() {
const health = useGameStore(state => state.health);
return<divclassName="health-bar"style={{width: `${health}%` }} />;
}