Loading...
Loading...
R3F performance optimization—LOD (Level of Detail), frustum culling, instancing strategies, draw call reduction, frame budgets, lazy loading, and profiling tools. Use when optimizing render performance, handling large scenes, or debugging frame rate issues.
npx skill4agent add bbeierle12/skill-mcp-claude r3f-performance// 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>| Phase | Target | Notes |
|---|---|---|
| JavaScript | < 4ms | useFrame logic, state updates |
| GPU Render | < 10ms | Draw calls, shaders |
| Compositing | < 2ms | Post-processing, overlays |
| Buffer | ~1ms | Safety margin |
| 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 |
// 10,000 cubes = 1 draw call
<instancedMesh args={[undefined, undefined, 10000]}>
<boxGeometry />
<meshStandardMaterial />
</instancedMesh>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>
);
}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} />;
}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>
);
}// 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]);<Canvas
performance={{
min: 0.5, // Minimum DPR under stress
max: 1, // Maximum DPR
debounce: 200 // Debounce time for changes (ms)
}}
/>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
};
}import { Suspense, lazy } from 'react';
const HeavyModel = lazy(() => import('./HeavyModel'));
function Scene() {
return (
<Suspense fallback={<SimpleLoader />}>
<HeavyModel />
</Suspense>
);
}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');
}, []);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>
);
}// 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');
};
}, []);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');import { Stats } from '@react-three/drei';
<Canvas>
<Stats /> {/* FPS, MS, MB counters */}
<Scene />
</Canvas>import { Perf } from 'r3f-perf';
<Canvas>
<Perf
position="top-left"
showGraph // Show FPS graph
minimal={false} // Full or minimal view
/>
<Scene />
</Canvas>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;
}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;
}| 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 |
[ ] 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 disabledr3f-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 configreferences/profiling-guide.mdreferences/mobile-optimization.mdreferences/large-scenes.md