R3F Performance
Optimize render performance through draw call reduction, geometry optimization, smart loading, and profiling.
Quick Start
// Performance-optimized Canvas
<Canvas
dpr={[1, 2]} // Limit pixel ratio
performance={{ min: 0.5 }} // Adaptive performance
frameloop="demand" // Only render on change
gl={{
powerPreference: 'high-performance',
antialias: false // Disable for mobile
}}
>
<Suspense fallback={null}>
<Scene />
</Suspense>
</Canvas>
Frame Budget
Target 60fps = 16.67ms per frame. Budget breakdown:
| Phase | Target | Notes | |-------|--------|-------| | JavaScript | < 4ms | useFrame logic, state updates | | GPU Render | < 10ms | Draw calls, shaders | | Compositing | < 2ms | Post-processing, overlays | | Buffer | ~1ms | Safety margin |
Draw Call Optimization
The Golden Rule
Fewer draw calls > fewer triangles
A scene with 100 meshes of 1000 triangles each is slower than 1 mesh of 100,000 triangles.
Reduction Techniques
| Technique | Draw Calls | When to Use | |-----------|------------|-------------| | Instancing | 1 per unique mesh | 100+ identical objects | | Merged geometry | 1 per merged batch | Static scene parts | | Texture atlases | Fewer materials | Many similar textures | | LOD | Reduces complexity | Large/distant objects |
Instancing (Best for Identical Meshes)
// 10,000 cubes = 1 draw call
<instancedMesh args={[undefined, undefined, 10000]}>
<boxGeometry />
<meshStandardMaterial />
</instancedMesh>
Geometry Merging (Static Scenes)
import { useMemo } from 'react';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils';
import * as THREE from 'three';
function MergedScene() {
const mergedGeometry = useMemo(() => {
const geometries: THREE.BufferGeometry[] = [];
// Create many positioned geometries
for (let i = 0; i < 100; i++) {
const geo = new THREE.BoxGeometry(1, 1, 1);
geo.translate(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
geometries.push(geo);
}
return mergeGeometries(geometries);
}, []);
return (
<mesh geometry={mergedGeometry}>
<meshStandardMaterial />
</mesh>
);
}
Level of Detail (LOD)
Swap geometry based on camera distance:
import { useMemo } from 'react';
import * as THREE from 'three';
function LODMesh() {
const lod = useMemo(() => {
const lodObject = new THREE.LOD();
// High detail (close)
const highGeo = new THREE.SphereGeometry(1, 64, 64);
const highMesh = new THREE.Mesh(highGeo, new THREE.MeshStandardMaterial({ color: 'red' }));
lodObject.addLevel(highMesh, 0);
// Medium detail
const medGeo = new THREE.SphereGeometry(1, 32, 32);
const medMesh = new THREE.Mesh(medGeo, new THREE.MeshStandardMaterial({ color: 'orange' }));
lodObject.addLevel(medMesh, 10);
// Low detail (far)
const lowGeo = new THREE.SphereGeometry(1, 8, 8);
const lowMesh = new THREE.Mesh(lowGeo, new THREE.MeshStandardMaterial({ color: 'green' }));
lodObject.addLevel(lowMesh, 30);
return lodObject;
}, []);
return <primitive object={lod} />;
}
Drei LOD Helper
import { Detailed } from '@react-three/drei';
function AdaptiveSphere() {
return (
<Detailed distances={[0, 10, 30]}>
{/* Close: high detail */}
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial />
</mesh>
{/* Medium distance */}
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial />
</mesh>
{/* Far: low detail */}
<mesh>
<sphereGeometry args={[1, 8, 8]} />
<meshStandardMaterial />
</mesh>
</Detailed>
);
}
Frustum Culling
Objects outside camera view are not rendered. Enabled by default, but:
// Disable for objects that animate into view unpredictably
<mesh frustumCulled={false}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
// Force bounding sphere update for dynamic geometry
useEffect(() => {
geometry.computeBoundingSphere();
}, [geometry]);
Adaptive Performance
R3F's adaptive performance system automatically adjusts DPR:
<Canvas
performance={{
min: 0.5, // Minimum DPR under stress
max: 1, // Maximum DPR
debounce: 200 // Debounce time for changes (ms)
}}
/>
Manual Performance Control
import { useThree } from '@react-three/fiber';
function PerformanceMonitor() {
const { performance } = useThree();
useFrame(() => {
// Check current performance
if (performance.current < 1) {
// System is under stress, reduce complexity
}
});
// Trigger performance drop
const triggerRegress = () => {
performance.regress(); // Temporarily lower DPR
};
}
Lazy Loading
Code Splitting with Suspense
import { Suspense, lazy } from 'react';
const HeavyModel = lazy(() => import('./HeavyModel'));
function Scene() {
return (
<Suspense fallback={<SimpleLoader />}>
<HeavyModel />
</Suspense>
);
}
Progressive Loading
import { useGLTF } from '@react-three/drei';
function Model() {
// Preload in background
useGLTF.preload('/model.glb');
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
// Preload before component mounts
useEffect(() => {
useGLTF.preload('/next-model.glb');
}, []);
View-Based Loading
import { useInView } from 'react-intersection-observer';
function LazySection() {
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: '200px' // Start loading 200px before visible
});
return (
<group ref={ref}>
{inView && <HeavyContent />}
</group>
);
}
Memory Management
Dispose Unused Resources
// Manual disposal
useEffect(() => {
return () => {
geometry.dispose();
material.dispose();
texture.dispose();
};
}, []);
// Drei helper for GLTF
import { useGLTF } from '@react-three/drei';
useEffect(() => {
return () => {
useGLTF.clear('/model.glb');
};
}, []);
Texture Optimization
import { useTexture } from '@react-three/drei';
import * as THREE from 'three';
// Compress and optimize
const texture = useTexture('/texture.jpg', (tex) => {
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.generateMipmaps = true;
tex.anisotropy = 4; // Lower = faster, higher = sharper
});
// Use compressed formats (KTX2)
import { useKTX2 } from '@react-three/drei';
const texture = useKTX2('/texture.ktx2');
Profiling
Stats Panel
import { Stats } from '@react-three/drei';
<Canvas>
<Stats /> {/* FPS, MS, MB counters */}
<Scene />
</Canvas>
Performance Panel
import { Perf } from 'r3f-perf';
<Canvas>
<Perf
position="top-left"
showGraph // Show FPS graph
minimal={false} // Full or minimal view
/>
<Scene />
</Canvas>
Manual Profiling
import { useThree } from '@react-three/fiber';
function ProfileInfo() {
const { gl } = useThree();
useEffect(() => {
const info = gl.info;
console.log({
drawCalls: info.render.calls,
triangles: info.render.triangles,
points: info.render.points,
lines: info.render.lines,
textures: info.memory.textures,
geometries: info.memory.geometries
});
});
return null;
}
Frame Time Measurement
function FrameProfiler() {
const frameTimeRef = useRef<number[]>([]);
useFrame(() => {
const start = performance.now();
// ... your logic ...
const elapsed = performance.now() - start;
frameTimeRef.current.push(elapsed);
if (frameTimeRef.current.length > 60) {
const avg = frameTimeRef.current.reduce((a, b) => a + b) / 60;
console.log(`Avg frame time: ${avg.toFixed(2)}ms`);
frameTimeRef.current = [];
}
});
return null;
}
Common Bottlenecks
| Symptom | Likely Cause | Fix | |---------|--------------|-----| | Low FPS, high draw calls | Too many meshes | Instance, merge, or LOD | | Low FPS, few draw calls | Heavy shaders/materials | Simplify shaders, use cheaper materials | | Stuttering on load | Large assets | Lazy load, compress, use LOD | | Memory growth | No disposal | Dispose on unmount | | Mobile issues | High DPR, AA | Limit DPR, disable antialias |
Optimization Checklist
[ ] Draw calls < 100 for complex scenes
[ ] Instancing for repeated objects
[ ] LOD for large/distant objects
[ ] Geometry merged where possible
[ ] Textures compressed (KTX2/Basis)
[ ] DPR capped at 2
[ ] Lazy loading for heavy assets
[ ] Proper disposal on unmount
[ ] Frustum culling enabled
[ ] Shadows optimized or disabled
File Structure
r3f-performance/
├── SKILL.md
├── references/
│ ├── profiling-guide.md # Deep profiling techniques
│ ├── mobile-optimization.md # Mobile-specific tips
│ └── large-scenes.md # Handling massive scenes
└── scripts/
├── utils/
│ ├── lod-helper.ts # LOD setup utilities
│ ├── merge-helper.ts # Geometry merging
│ └── perf-monitor.ts # Performance monitoring
└── presets/
├── mobile.ts # Mobile-optimized Canvas config
└── desktop.ts # Desktop-optimized Canvas config
Reference
references/profiling-guide.md— Deep profiling with browser DevToolsreferences/mobile-optimization.md— Mobile-specific optimizationreferences/large-scenes.md— Handling 100k+ object scenes