
R3f Best Practices
- 915 installs
- 42 repo stars
- Updated January 28, 2026
- emalorenzo/three-agent-skills
r3f-best-practices is a Claude Code frontend skill that automatically enforces React Three Fiber performance, rendering, and component conventions for developers building agent-assisted 3D web experiences with Three.js a
About
r3f-best-practices is a rule-driven skill in emalorenzo/three-agent-skills that enforces React Three Fiber conventions during agent-assisted 3D development. The readme organizes guidance into priority tiers covering performance and re-renders, useFrame animation, and component patterns, with named rules such as perf-zustand-selectors, frame-delta-time, and component-attach-prop. At least 22 explicit rules appear across the excerpted sections, spanning state isolation, transient subscriptions, dispose patterns, render-on-demand, and r3f-perf profiling. Developers reach for r3f-best-practices when agents generate R3F scenes that stutter, leak GPU resources, or misuse useFrame. The skill acts as a continuous linter mindset for WebGL React code rather than a one-time tutorial.
- 70 prioritized rules across 8 severity tiers from CRITICAL performance to MEDIUM events & interaction
- Covers perf-never-set-state-in-useframe, zustand selectors, useFrame patterns, Drei helpers, Suspense loading, and canva
- Hard-gated review workflow that surfaces violations before code is committed
- Designed for Claude Code, Cursor, and other agents to prevent common R3F re-render and memory issues
R3f Best Practices by the numbers
- 915 all-time installs (skills.sh)
- +46 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #153 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/emalorenzo/three-agent-skills --skill r3f-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 915 |
|---|---|
| repo stars | ★ 42 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 28, 2026 |
| Repository | emalorenzo/three-agent-skills ↗ |
How do you optimize React Three Fiber performance?
Automatically enforce React Three Fiber performance, rendering, and component best practices during agent-assisted 3D development.
Who is it for?
Frontend developers building React Three Fiber or drei scenes with coding agents who need enforced WebGL performance and component conventions.
Skip if: Developers writing vanilla Three.js without React, native game engines, or 2D React apps with no WebGL canvas.
When should I use this skill?
An agent writes or refactors React Three Fiber code involving useFrame, zustand state, drei primitives, or 3D scene performance issues.
What you get
R3F code following perf, useFrame, and component rules with corrected re-render, animation, and disposal patterns.
- R3F code conforming to performance rules
- refactored component and useFrame patterns
By the numbers
- Documents 22+ named rules across three priority tiers in the readme excerpt
- Lists 10 CRITICAL performance and re-render rules in Priority 1 alone
Files
React Three Fiber Best Practices
Comprehensive guide for React Three Fiber and the Poimandres ecosystem. Contains 70+ rules across 12 categories, prioritized by impact.
Sources & Credits
Additional tips from 100 Three.js Tips by Utsubo
When to Apply
Reference these guidelines when:
- Writing new R3F components
- Optimizing R3F performance (re-renders are the #1 issue)
- Using Drei helpers correctly
- Managing state with Zustand
- Implementing post-processing or physics
Ecosystem Coverage
- @react-three/fiber - React renderer for Three.js
- @react-three/drei - Useful helpers and abstractions
- @react-three/postprocessing - Post-processing effects
- @react-three/rapier - Physics engine
- zustand - State management
- leva - Debug GUI
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Performance & Re-renders | CRITICAL | perf- |
| 2 | useFrame & Animation | CRITICAL | frame- |
| 3 | Component Patterns | HIGH | component- |
| 4 | Canvas & Setup | HIGH | canvas- |
| 5 | Drei Helpers | MEDIUM-HIGH | drei- |
| 6 | Loading & Suspense | MEDIUM-HIGH | loading- |
| 7 | State Management | MEDIUM | state- |
| 8 | Events & Interaction | MEDIUM | events- |
| 9 | Post-processing | MEDIUM | postpro- |
| 10 | Physics (Rapier) | LOW-MEDIUM | physics- |
| 11 | Leva (Debug GUI) | LOW | leva- |
Quick Reference
1. Performance & Re-renders (CRITICAL)
perf-never-set-state-in-useframe- NEVER call setState in useFrameperf-isolate-state- Isolate components that need React stateperf-zustand-selectors- Use Zustand selectors, not entire storeperf-transient-subscriptions- Use transient subscriptions for continuous valuesperf-memo-components- Memoize expensive componentsperf-keys-for-lists- Use stable keys for dynamic listsperf-avoid-inline-objects- Avoid creating objects/arrays in JSXperf-dispose-auto- Understand R3F auto-dispose behaviorperf-visibility-toggle- Toggle visibility instead of remountingperf-r3f-perf- Use r3f-perf for performance monitoring
2. useFrame & Animation (CRITICAL)
frame-priority- Use priority for execution orderframe-delta-time- Always use delta for animationsframe-conditional-subscription- Disable useFrame when not neededframe-destructure-state- Destructure only what you needframe-render-on-demand- Use invalidate() for on-demand renderingframe-avoid-heavy-computation- Move heavy work outside useFrame
3. Component Patterns (HIGH)
component-jsx-elements- Use JSX for Three.js objectscomponent-attach-prop- Use attach for non-standard propertiescomponent-primitive- Use primitive for existing objectscomponent-extend- Use extend() for custom classescomponent-forwardref- Use forwardRef for reusable componentscomponent-dispose-null- Set dispose={null} on shared resources
4. Canvas & Setup (HIGH)
canvas-size-container- Canvas fills parent containercanvas-camera-default- Configure camera via propcanvas-gl-config- Configure WebGL contextcanvas-shadows- Enable shadows at Canvas levelcanvas-frameloop- Choose appropriate frameloop modecanvas-events- Configure event handlingcanvas-linear-flat- Use linear/flat for correct colors
5. Drei Helpers (MEDIUM-HIGH)
drei-use-gltf- useGLTF with preloadingdrei-use-texture- useTexture for texture loadingdrei-environment- Environment for realistic lightingdrei-orbit-controls- OrbitControls from Dreidrei-html- Html for DOM overlaysdrei-text- Text for 3D textdrei-instances- Instances for optimized instancingdrei-use-helper- useHelper for debug visualizationdrei-bounds- Bounds to fit cameradrei-center- Center to center objectsdrei-float- Float for floating animation
6. Loading & Suspense (MEDIUM-HIGH)
loading-suspense- Wrap async components in Suspenseloading-preload- Preload assets with useGLTF.preloadloading-use-progress- useProgress for loading UIloading-lazy-components- Lazy load heavy componentsloading-error-boundary- Handle loading errors
7. State Management (MEDIUM)
state-zustand-store- Create focused Zustand storesstate-avoid-objects-in-store- Be careful with Three.js objectsstate-subscribeWithSelector- Fine-grained subscriptionsstate-persist- Persist state when neededstate-separate-concerns- Separate stores by concern
8. Events & Interaction (MEDIUM)
events-pointer-events- Use pointer events on meshesevents-stop-propagation- Prevent event bubblingevents-cursor-pointer- Change cursor on hoverevents-raycast-filter- Filter raycastingevents-event-data- Understand event data structure
9. Post-processing (MEDIUM)
postpro-effect-composer- Use EffectComposerpostpro-common-effects- Common effects referencepostpro-selective-bloom- SelectiveBloom for optimized glowpostpro-custom-shader- Create custom effectspostpro-performance- Optimize post-processing
10. Physics Rapier (LOW-MEDIUM)
physics-setup- Basic Rapier setupphysics-body-types- dynamic, fixed, kinematicphysics-colliders- Choose appropriate collidersphysics-events- Handle collision eventsphysics-api-ref- Use ref for physics APIphysics-performance- Optimize physics
11. Leva (LOW)
leva-basic- Basic Leva usageleva-folders- Organize with foldersleva-conditional- Hide in production
How to Use
Read individual rule files for detailed explanations and code examples:
rules/perf-never-set-state-in-useframe.md
rules/drei-use-gltf.md
rules/state-zustand-selectors.mdFull Compiled Document
For the complete guide with all rules expanded: ../R3F_BEST_PRACTICES.md
Critical Patterns
NEVER setState in useFrame
// BAD - 60 re-renders per second!
function BadComponent() {
const [position, setPosition] = useState(0);
useFrame(() => {
setPosition(p => p + 0.01); // NEVER DO THIS
});
return <mesh position-x={position} />;
}
// GOOD - Mutate refs directly
function GoodComponent() {
const meshRef = useRef();
useFrame(() => {
meshRef.current.position.x += 0.01;
});
return <mesh ref={meshRef} />;
}Zustand Selectors
// BAD - Re-renders on ANY store change
const store = useGameStore();
// GOOD - Only re-renders when playerX changes
const playerX = useGameStore(state => state.playerX);
// BETTER - No re-renders, direct mutation
useFrame(() => {
const { value } = useStore.getState();
ref.current.position.x = value;
});Drei useGLTF
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
// Preload for instant loading
useGLTF.preload('/model.glb');Suspense Loading
function App() {
return (
<Canvas>
<Suspense fallback={<Loader />}>
<Model />
</Suspense>
</Canvas>
);
}r3f-perf Monitoring
import { Perf } from 'r3f-perf';
function App() {
return (
<Canvas>
<Perf position="top-left" />
<Scene />
</Canvas>
);
}Toggle Visibility (Not Remounting)
// BAD: Remounting destroys and recreates
{showModel && <Model />}
// GOOD: Toggle visibility, keeps instance alive
<Model visible={showModel} />Rule Sections
Priority 1: Performance & Re-renders (CRITICAL)
- perf-never-set-state-in-useframe
- perf-isolate-state
- perf-zustand-selectors
- perf-transient-subscriptions
- perf-memo-components
- perf-keys-for-lists
- perf-avoid-inline-objects
- perf-dispose-auto
- perf-visibility-toggle
- perf-r3f-perf
Priority 2: useFrame & Animation (CRITICAL)
- frame-priority
- frame-delta-time
- frame-conditional-subscription
- frame-destructure-state
- frame-render-on-demand
- frame-avoid-heavy-computation
Priority 3: Component Patterns (HIGH)
- component-jsx-elements
- component-attach-prop
- component-primitive
- component-extend
- component-forwardref
- component-dispose-null
Priority 4: Canvas & Setup (HIGH)
- canvas-size-container
- canvas-camera-default
- canvas-gl-config
- canvas-shadows
- canvas-frameloop
- canvas-events
- canvas-linear-flat
Priority 5: Drei Helpers (MEDIUM-HIGH)
- drei-use-gltf
- drei-use-texture
- drei-environment
- drei-orbit-controls
- drei-html
- drei-text
- drei-instances
- drei-use-helper
- drei-bounds
- drei-center
- drei-float
Priority 6: Loading & Suspense (MEDIUM-HIGH)
- loading-suspense
- loading-preload
- loading-use-progress
- loading-lazy-components
- loading-error-boundary
Priority 7: State Management (MEDIUM)
- state-zustand-store
- state-avoid-objects-in-store
- state-subscribeWithSelector
- state-persist
- state-separate-concerns
Priority 8: Events & Interaction (MEDIUM)
- events-pointer-events
- events-stop-propagation
- events-cursor-pointer
- events-raycast-filter
- events-event-data
Priority 9: Post-processing (MEDIUM)
- postpro-effect-composer
- postpro-common-effects
- postpro-selective-bloom
- postpro-custom-shader
- postpro-performance
Priority 10: Physics Rapier (LOW-MEDIUM)
- physics-setup
- physics-body-types
- physics-colliders
- physics-events
- physics-api-ref
- physics-performance
Priority 11: Leva Debug GUI (LOW)
- leva-basic
- leva-folders
- leva-conditional
drei-use-gltf
Use useGLTF for model loading with preloading.
Why It Matters
useGLTF from Drei provides:
- Suspense integration (automatic loading states)
- Caching (same model loaded once)
- Preloading capability
- Draco decompression support
Basic Example
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene, nodes, materials } = useGLTF('/model.glb');
return <primitive object={scene} />;
}Preloading (Critical for UX)
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
// Preload at module level - starts loading immediately
useGLTF.preload('/model.glb');
// Or preload multiple models
useGLTF.preload(['/model1.glb', '/model2.glb', '/model3.glb']);With Draco Compression
function DracoModel() {
// Second argument is the Draco decoder path
const { scene } = useGLTF('/model.glb', '/draco/');
return <primitive object={scene} />;
}
useGLTF.preload('/model.glb', '/draco/');Accessing Nodes and Materials
function Character() {
const { nodes, materials } = useGLTF('/character.glb');
return (
<group>
<mesh
geometry={nodes.Body.geometry}
material={materials.Skin}
/>
<mesh
geometry={nodes.Clothes.geometry}
material={materials.Fabric}
/>
</group>
);
}Clone for Multiple Instances
function Tree({ position }) {
const { scene } = useGLTF('/tree.glb');
// Clone to avoid sharing state between instances
return <primitive object={scene.clone()} position={position} />;
}
function Forest() {
return (
<>
<Tree position={[0, 0, 0]} />
<Tree position={[5, 0, 0]} />
<Tree position={[10, 0, 0]} />
</>
);
}With Suspense
import { Suspense } from 'react';
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
function Scene() {
return (
<Suspense fallback={<LoadingBox />}>
<Model />
</Suspense>
);
}
function LoadingBox() {
return (
<mesh>
<boxGeometry />
<meshBasicMaterial wireframe color="white" />
</mesh>
);
}TypeScript Types
import { useGLTF } from '@react-three/drei';
import { GLTF } from 'three-stdlib';
type GLTFResult = GLTF & {
nodes: {
Body: THREE.Mesh;
Head: THREE.Mesh;
};
materials: {
Skin: THREE.MeshStandardMaterial;
};
};
function Model() {
const { nodes, materials } = useGLTF('/model.glb') as GLTFResult;
// Now nodes.Body and materials.Skin are typed
}Dispose Handling
R3F auto-disposes by default. To prevent disposal (for shared models):
function SharedModel({ model }) {
return <primitive object={model} dispose={null} />;
}References
- Drei useGLTF
- gltfjsx - Generate JSX from GLTF
frame-delta-time
Always use delta for frame-rate independent animation.
Why It Matters
Without delta time, animations run faster on 144Hz displays and slower on 30fps mobile devices. Delta time ensures consistent animation speed regardless of frame rate.
Bad Example
// BAD - Speed varies with frame rate
function BadAnimation() {
const meshRef = useRef();
useFrame(() => {
meshRef.current.rotation.y += 0.01; // Fast on 144hz, slow on 30hz
});
return <mesh ref={meshRef} />;
}Good Example
// GOOD - Consistent speed on all devices
function GoodAnimation() {
const meshRef = useRef();
useFrame((state, delta) => {
meshRef.current.rotation.y += 1 * delta; // 1 radian per second, always
});
return <mesh ref={meshRef} />;
}Using Clock for Time-based Effects
function TimeBasedAnimation() {
const meshRef = useRef();
useFrame(({ clock }) => {
const t = clock.elapsedTime;
// Oscillate at consistent frequency
meshRef.current.position.y = Math.sin(t * 2) * 2; // 2 Hz oscillation
meshRef.current.position.x = Math.cos(t) * 3;
});
return <mesh ref={meshRef} />;
}Frame-rate Independent Lerp
function SmoothFollow({ target }) {
const meshRef = useRef();
useFrame((state, delta) => {
// Frame-rate independent smooth interpolation
const lerpFactor = 1 - Math.pow(0.001, delta);
meshRef.current.position.lerp(target, lerpFactor);
});
return <mesh ref={meshRef} />;
}Movement with Speed
function MovingObject() {
const meshRef = useRef();
const speed = 5; // units per second
const direction = useRef(new THREE.Vector3(1, 0, 0));
useFrame((state, delta) => {
meshRef.current.position.addScaledVector(direction.current, speed * delta);
});
return <mesh ref={meshRef} />;
}Combining Delta and Elapsed Time
function ComplexAnimation() {
const meshRef = useRef();
useFrame(({ clock }, delta) => {
const t = clock.elapsedTime;
// Position based on elapsed time (consistent patterns)
meshRef.current.position.x = Math.sin(t) * 3;
meshRef.current.position.z = Math.cos(t) * 3;
// Rotation based on delta (consistent speed)
meshRef.current.rotation.y += 0.5 * delta;
});
return <mesh ref={meshRef} />;
}State Destructuring
useFrame(({ clock, camera, pointer, viewport }, delta) => {
// clock.elapsedTime - total time since start
// clock.getDelta() - same as delta parameter
// camera - the default camera
// pointer - normalized mouse position (-1 to 1)
// viewport - { width, height, factor }
});References
loading-suspense
Wrap async components in Suspense.
Why It Matters
R3F integrates with React Suspense for loading states. Components using useGLTF, useTexture, or other async loaders will suspend and need a Suspense boundary with a fallback.
Basic Example
import { Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
function App() {
return (
<Canvas>
<Suspense fallback={<LoadingFallback />}>
<Model />
</Suspense>
</Canvas>
);
}
function LoadingFallback() {
return (
<mesh>
<boxGeometry />
<meshBasicMaterial wireframe color="white" />
</mesh>
);
}Multiple Async Components
function Scene() {
return (
<Suspense fallback={<Loader />}>
<Environment preset="city" />
<Model url="/character.glb" />
<Ground />
</Suspense>
);
}Nested Suspense Boundaries
function Scene() {
return (
<>
{/* Environment loads first */}
<Suspense fallback={null}>
<Environment preset="sunset" />
</Suspense>
{/* Main content with visible loader */}
<Suspense fallback={<Loader />}>
<Character />
<Props />
</Suspense>
{/* Background loads last, no blocking */}
<Suspense fallback={null}>
<BackgroundDetails />
</Suspense>
</>
);
}Using useProgress
import { useProgress, Html } from '@react-three/drei';
function Loader() {
const { active, progress, errors, item, loaded, total } = useProgress();
return (
<Html center>
<div className="loader">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
</div>
<p>{progress.toFixed(0)}% loaded</p>
<p className="loading-item">{item}</p>
</div>
</Html>
);
}
function App() {
return (
<Canvas>
<Suspense fallback={<Loader />}>
<Scene />
</Suspense>
</Canvas>
);
}Error Boundaries
import { ErrorBoundary } from 'react-error-boundary';
function ModelErrorFallback({ error, resetErrorBoundary }) {
return (
<Html center>
<div>
<p>Failed to load model</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
</Html>
);
}
function SafeModel({ url }) {
return (
<ErrorBoundary
FallbackComponent={ModelErrorFallback}
onReset={() => {
// Reset any state if needed
}}
>
<Suspense fallback={<LoadingBox />}>
<Model url={url} />
</Suspense>
</ErrorBoundary>
);
}Preloading to Avoid Suspense
import { useGLTF, useTexture } from '@react-three/drei';
// Preload at module level
useGLTF.preload('/model.glb');
useTexture.preload('/texture.png');
// Component won't suspend if already loaded
function Model() {
const { scene } = useGLTF('/model.glb'); // Instant if preloaded
return <primitive object={scene} />;
}Animated Fallback
function AnimatedLoader() {
const meshRef = useRef();
useFrame((state) => {
meshRef.current.rotation.x = state.clock.elapsedTime;
meshRef.current.rotation.y = state.clock.elapsedTime * 0.5;
});
return (
<mesh ref={meshRef}>
<icosahedronGeometry args={[1, 1]} />
<meshBasicMaterial wireframe color="cyan" />
</mesh>
);
}References
perf-never-set-state-in-useframe
NEVER call setState inside useFrame.
Why It Matters
This is the #1 performance killer in R3F. Calling setState triggers React re-renders. useFrame runs at 60fps. setState in useFrame = 60 re-renders per second = destroyed performance.
Bad Example
// BAD - Causes 60 re-renders per second!
function BadComponent() {
const [position, setPosition] = useState(0);
useFrame(() => {
setPosition(p => p + 0.01); // NEVER DO THIS
});
return <mesh position-x={position} />;
}This triggers React reconciliation 60 times per second, causing:
- Massive CPU usage
- Frame drops
- Component re-creation
- Garbage collection pauses
Good Example
// GOOD - Mutate refs directly, no re-renders
function GoodComponent() {
const meshRef = useRef();
useFrame(() => {
meshRef.current.position.x += 0.01;
});
return <mesh ref={meshRef} />;
}The ref gives direct access to the Three.js object. Mutating it doesn't trigger React.
With Delta Time
function AnimatedComponent() {
const meshRef = useRef();
useFrame((state, delta) => {
meshRef.current.rotation.y += 1 * delta; // 1 rad/sec
meshRef.current.position.x = Math.sin(state.clock.elapsedTime);
});
return <mesh ref={meshRef} />;
}Multiple Values
function ComplexAnimation() {
const meshRef = useRef();
useFrame(({ clock }) => {
const t = clock.elapsedTime;
meshRef.current.position.set(
Math.sin(t) * 2,
Math.cos(t * 2),
Math.sin(t * 0.5)
);
meshRef.current.rotation.set(t, t * 0.5, 0);
});
return <mesh ref={meshRef} />;
}Using Zustand Without Re-renders
function ZustandAnimation() {
const meshRef = useRef();
useFrame(() => {
// getState() doesn't trigger re-renders
const { targetPosition } = useStore.getState();
meshRef.current.position.lerp(targetPosition, 0.1);
});
return <mesh ref={meshRef} />;
}When You NEED State
If you genuinely need React state (e.g., for UI updates), isolate it:
// Keep animated mesh separate
function AnimatedMesh() {
const meshRef = useRef();
useFrame((_, delta) => {
meshRef.current.rotation.y += delta;
});
return <mesh ref={meshRef} />;
}
// State changes here won't affect AnimatedMesh
function UI() {
const [score, setScore] = useState(0);
return <Html><div>{score}</div></Html>;
}
function Scene() {
return (
<>
<AnimatedMesh />
<UI />
</>
);
}References
r3f-perf for Performance Monitoring
Source: 100 Three.js Tips - Utsubo
Use r3f-perf for comprehensive React Three Fiber performance monitoring.
Installation
npm install r3f-perfBasic Usage
import { Perf } from 'r3f-perf';
function App() {
return (
<Canvas>
<Perf position="top-left" />
<Scene />
</Canvas>
);
}Available Props
<Perf
position="top-left" // Position: top-left, top-right, bottom-left, bottom-right
minimal={false} // Minimal mode (just FPS)
showGraph={true} // Show performance graph
matrixUpdate={true} // Show matrix updates
deepAnalyze={false} // Deep analysis (more CPU intensive)
overClock={false} // Over-clock mode for high refresh rate monitors
logsPerSecond={10} // Logs per second
/>What It Monitors
- FPS - Frames per second
- MS - Milliseconds per frame
- CPU - JavaScript execution time
- GPU - WebGL draw time (estimated)
- Memory - Geometries, textures, draw calls
- Matrix Updates - Number of matrix recalculations
Conditional Rendering for Production
import { Perf } from 'r3f-perf';
function App() {
const isDev = process.env.NODE_ENV === 'development';
return (
<Canvas>
{isDev && <Perf position="top-left" />}
<Scene />
</Canvas>
);
}Deep Analyze Mode
For detailed per-object analysis:
<Perf
deepAnalyze={true}
className="custom-perf"
/>Shows render times for individual objects but has higher CPU overhead.
Best Practices
1. Remove in production - Always hide in production builds 2. Use sparingly - Performance monitor itself has overhead 3. Check draw calls - Target under 100 draw calls 4. Monitor memory - Watch for leaking geometries/textures 5. Use alongside renderer.info - For detailed WebGL stats
Toggle Visibility Instead of Remounting
Source: 100 Three.js Tips - Utsubo
Toggle the visible prop instead of conditionally mounting/unmounting components.
Why It Matters
Remounting a component: 1. Destroys the Three.js object 2. Triggers disposal (if configured) 3. Creates new geometry/material 4. Uploads new data to GPU 5. Recompiles shaders
Toggling visibility: 1. Sets object.visible = false 2. That's it - object stays in memory
BAD: Conditional Mounting
function Scene({ showModel }) {
return (
<>
{showModel && <ExpensiveModel />}
</>
);
}Every toggle destroys and recreates the entire model.
GOOD: Visibility Toggle
function Scene({ showModel }) {
return (
<ExpensiveModel visible={showModel} />
);
}Model stays in memory, just skipped during render.
When to Use Each
Use Visibility Toggle When:
- Frequent show/hide (e.g., UI state)
- Object is expensive to create
- Object is needed again soon
- Object count is manageable
Use Conditional Mounting When:
- Object is rarely shown
- Memory is constrained
- Object is cheap to create
- Large number of potential objects
With Refs
function ToggleableModel() {
const meshRef = useRef();
const [visible, setVisible] = useState(true);
// Direct mutation for animations
useFrame(() => {
if (meshRef.current) {
meshRef.current.visible = shouldBeVisible;
}
});
return <mesh ref={meshRef} visible={visible} />;
}Visibility vs Layers
For complex visibility logic, consider using Three.js layers:
function SelectiveRendering() {
const meshRef = useRef();
useEffect(() => {
// Set to layer 1 (not rendered by default camera)
meshRef.current.layers.set(1);
}, []);
return <mesh ref={meshRef} />;
}Layers allow camera-selective rendering without changing visibility.
perf-zustand-selectors
Use Zustand selectors to minimize re-renders.
Why It Matters
Subscribing to the entire Zustand store means your component re-renders whenever ANY value changes. Selectors let you subscribe to specific slices, so components only re-render when their specific data changes.
Bad Example
// BAD - Re-renders on ANY store change
function BadComponent() {
const store = useGameStore(); // Subscribes to entire store
return <mesh position-x={store.playerX} />;
}If score, health, or any other value changes, this component re-renders even though it only uses playerX.
Good Example
// GOOD - Re-renders only when playerX changes
function GoodComponent() {
const playerX = useGameStore(state => state.playerX);
return <mesh position-x={playerX} />;
}Multiple Values with Shallow
import { shallow } from 'zustand/shallow';
// GOOD - Re-renders only when x, y, or z changes
function PositionComponent() {
const { x, y, z } = useGameStore(
state => ({ x: state.x, y: state.y, z: state.z }),
shallow // Use shallow comparison for objects
);
return <mesh position={[x, y, z]} />;
}Transient Subscriptions (No Re-renders)
For values that change every frame (like positions), use transient subscriptions:
function TransientComponent() {
const meshRef = useRef();
useEffect(() => {
// Subscribe without causing re-renders
const unsubscribe = useGameStore.subscribe(
state => state.playerPosition,
position => {
meshRef.current.position.copy(position);
}
);
return unsubscribe;
}, []);
return <mesh ref={meshRef} />;
}getState() in useFrame
For animation, use getState() which doesn't subscribe:
function AnimatedComponent() {
const meshRef = useRef();
useFrame(() => {
// No subscription, no re-renders
const { targetPosition, speed } = useStore.getState();
meshRef.current.position.lerp(targetPosition, speed);
});
return <mesh ref={meshRef} />;
}Store Design for Performance
const useGameStore = create((set, get) => ({
// State
playerX: 0,
playerY: 0,
score: 0,
health: 100,
// Actions (don't subscribe to these)
movePlayer: (dx, dy) => set(state => ({
playerX: state.playerX + dx,
playerY: state.playerY + dy
})),
// Derived values as getters
getPlayerPosition: () => {
const { playerX, playerY } = get();
return new THREE.Vector3(playerX, playerY, 0);
}
}));Comparison
| Method | Re-renders | Use Case |
|---|---|---|
useStore() | Every change | Never use |
useStore(s => s.value) | When value changes | Most cases |
useStore(selector, shallow) | When any selected value changes | Multiple values |
useStore.subscribe() | Never | Continuous updates |
useStore.getState() | Never | Inside useFrame |
References
Related skills
How it compares
Choose r3f-best-practices over generic React performance skills when the codebase uses react-three-fiber, drei, and WebGL canvas rendering.
FAQ
What performance issues does r3f-best-practices address?
r3f-best-practices addresses React Three Fiber re-render storms, misuse of useFrame, missing dispose calls, inline object allocations, and Zustand subscription patterns. The skill prioritizes CRITICAL performance rules before component structure guidance.
Does r3f-best-practices cover animation loops?
r3f-best-practices includes a dedicated useFrame and animation section with rules for frame priority, delta time, conditional subscriptions, and render-on-demand. These rules target smooth 3D animation without heavy per-frame React state updates.
Is R3f Best Practices safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.