Agent skill
ta-r3f-performance
Performance optimization techniques for R3F and Three.js. Use when optimizing frame rate, reducing draw calls, mobile performance.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/ta-r3f-performance
SKILL.md
R3F Performance Skill
"Optimize for mobile, scale up for desktop – 60 FPS is the goal."
When to Use This Skill
Use when:
- FPS drops below 60
- Targeting mobile devices
- Rendering many objects
- Implementing LOD systems
- Debugging performance issues
Quick Start
// Performance-optimized Canvas
<Canvas
dpr={[1, 2]} // Limit pixel ratio
performance={{ min: 0.5 }} // Auto-reduce quality
gl={{ antialias: false }} // Disable for mobile
>
<Suspense fallback={null}>
<Scene />
</Suspense>
</Canvas>
The 16ms Budget (60 FPS)
| System | Budget | Notes |
|---|---|---|
| Input | ~1ms | Event handling |
| Physics | ~3ms | Rapier/Cannon updates |
| Game Logic | ~4ms | State, AI, animations |
| Render | ~5ms | Three.js draw calls |
| Buffer | ~3ms | Safety margin |
| Total | 16.67ms | 60 FPS target |
Performance Monitoring Setup
Always establish performance monitoring before optimizing. Use Stats.js from drei:
import { Stats } from '@react-three/drei';
function Scene() {
const showStats = import.meta.env.DEV;
return (
<>
{showStats && <Stats />}
{/* Your scene content */}
</>
);
}
// In App.tsx
<Canvas>
<Stats className="stats-position" />
<Scene />
</Canvas>
CSS for Stats Positioning
.stats-position {
position: absolute;
top: 0;
left: 0;
z-index: 1000;
}
Performance Benchmarks to Establish
| Metric | Target | How to Measure |
|---|---|---|
| FPS | 60+ | Stats.js |
| Frame Time | <16.67ms | Stats.js MS |
| Draw Calls | <100 | renderer.info.render.calls |
| Triangles | <100k | renderer.info.render.triangles |
| Textures | <50MB | renderer.info.memory.textures |
| Geometries | <20MB | renderer.info.memory.geometries |
Reading Renderer Info (Development)
import { useThree } from '@react-three/fiber';
function PerfMonitor() {
const { gl } = useThree();
useEffect(() => {
if (!import.meta.env.DEV) return;
const info = gl.info;
console.log('Render Info:', {
calls: info.render.calls,
triangles: info.render.triangles,
points: info.render.points,
lines: info.render.lines,
textures: info.memory.textures,
geometries: info.memory.geometries,
});
}, [gl]);
return null;
}
Decision Framework
| Symptom | Likely Cause | Solution |
|---|---|---|
| Low FPS everywhere | Too many draw calls | Instancing, merging |
| FPS drops on zoom | LOD not implemented | Add LOD system |
| Mobile slow | DPR too high | Limit to 1.5 |
| Memory grows | Dispose missing | Add cleanup |
| Stuttering | GC pressure | Object pooling |
Progressive Guide
Level 1: Basic Optimizations
// Limit device pixel ratio
<Canvas dpr={Math.min(window.devicePixelRatio, 2)}>
// Disable expensive features on mobile
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
<Canvas
shadows={!isMobile}
gl={{
antialias: !isMobile,
powerPreference: 'high-performance',
}}
>
Level 2: Instanced Rendering
import { Instances, Instance } from '@react-three/drei';
// Instead of 1000 separate meshes
function OptimizedTrees({ positions }) {
return (
<Instances limit={positions.length}>
<cylinderGeometry args={[0.1, 0.3, 2]} />
<meshStandardMaterial color="brown" />
{positions.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
);
}
Level 3: Level of Detail (LOD)
Using Drei's <Detailed> Component
import { Detailed, useGLTF } from '@react-three/drei';
function LODTree({ position }: { position: [number, number, number] }) {
// Load all LOD levels at once
const [low, mid, high] = useGLTF([
'/assets/models/tree-low.glb',
'/assets/models/tree-mid.glb',
'/assets/models/tree-high.glb',
]);
return (
<Detailed
distances={[0, 10, 30]} // Distance thresholds for each LOD
position={position}
>
{/* 0-10 units: High detail */}
<primitive object={high.scene} />
{/* 10-30 units: Medium detail */}
<primitive object={mid.scene} />
{/* 30+ units: Low detail */}
<primitive object={low.scene} />
</Detailed>
);
}
Using Native Three.js LOD
import { useRef, useEffect } from 'react';
import { useGLTF } from '@react-three/drei';
import * as THREE from 'three';
function NativeLODTree({ position }: { position: [number, number, number] }) {
const lodRef = useRef<THREE.LOD>(null);
const [low, mid, high] = useGLTF([
'/assets/models/tree-low.glb',
'/assets/models/tree-mid.glb',
'/assets/models/tree-high.glb',
]);
useEffect(() => {
if (!lodRef.current) return;
// Add LOD levels with distance thresholds
lodRef.current.addLevel(high.scene.clone(), 0); // 0-10 units
lodRef.current.addLevel(mid.scene.clone(), 10); // 10-30 units
lodRef.current.addLevel(low.scene.clone(), 30); // 30+ units
// Optional: Auto-update LOD based on camera
lodRef.current.update(camera);
}, [high, mid, low]);
return <lod ref={lodRef} position={position} />;
}
Billboard LOD for Distance Objects
import { useRef, useEffect } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { TextureLoader } from 'three';
function BillboardLOD({
textureUrl,
position,
}: {
textureUrl: string;
position: [number, number, number];
}) {
const meshRef = useRef<THREE.Mesh>(null);
const texture = new TextureLoader().load(textureUrl);
const { camera } = useThree();
// Make billboard always face camera
useFrame(() => {
if (meshRef.current && camera) {
meshRef.current.lookAt(camera.position);
}
});
return (
<mesh ref={meshRef} position={position}>
<planeGeometry args={[2, 4]} />
<meshBasicMaterial
map={texture}
transparent
side={THREE.DoubleSide}
/>
</mesh>
);
}
LOD Distance Guidelines
| Object Type | Near (High) | Mid | Far (Low) | Billboard |
|---|---|---|---|---|
| Trees | 0-15m | 15-40m | 40-100m | 100m+ |
| Buildings | 0-20m | 20-60m | 60-150m | 150m+ |
| Characters | 0-10m | 10-25m | 25-50m | - |
| Props | 0-10m | 10-30m | 30m+ | - |
Polygon Budget per LOD Level
| Asset | LOD0 (Near) | LOD1 (Mid) | LOD2 (Far) | Billboard |
|---|---|---|---|---|
| Character | 15K tris | 5K tris | 1K tris | N/A |
| Vehicle | 20K tris | 8K tris | 2K tris | N/A |
| Tree | 5K tris | 1K tris | 500 tris | 2 tris |
| Building | 10K tris | 3K tris | 1K tris | 2 tris |
Learned from bugfix-005 retrospective (2026-01-22):
- LOD systems reduce GPU workload for distant objects
- Use
<Detailed>from Drei for simpler React integration - Consider billboards for very distant objects (trees, props)
Level 4: Frustum Culling & BVH
import { useBVH } from '@react-three/drei';
function OptimizedMesh() {
const meshRef = useRef();
// Enable BVH for faster raycasting
useBVH(meshRef);
return (
<mesh ref={meshRef} frustumCulled>
<complexGeometry />
<meshStandardMaterial />
</mesh>
);
}
Level 5: Object Pooling
// Pool for frequently created/destroyed objects
const bulletPool = useMemo(() => {
const pool = [];
for (let i = 0; i < 100; i++) {
pool.push({
active: false,
position: new THREE.Vector3(),
velocity: new THREE.Vector3(),
});
}
return pool;
}, []);
function getBullet() {
return bulletPool.find((b) => !b.active);
}
function releaseBullet(bullet) {
bullet.active = false;
}
Mobile Optimization
| Feature | Desktop | Mobile |
|---|---|---|
| Pixel Ratio | 2.0 | 1.0-1.5 |
| Shadows | On | Off |
| Anti-aliasing | MSAA | Off |
| Post-processing | Full | Minimal |
| Draw calls | < 200 | < 50 |
| Polygons | < 1M | < 100K |
// Mobile detection and config
const config = useMemo(() => {
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
return {
dpr: isMobile ? 1 : Math.min(window.devicePixelRatio, 2),
shadows: !isMobile,
antialias: !isMobile,
maxDrawCalls: isMobile ? 50 : 200,
};
}, []);
Memory Management
// CRITICAL: Dispose of Three.js objects
useEffect(() => {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
return () => {
geometry.dispose();
material.dispose();
// Also dispose textures
if (material.map) material.map.dispose();
};
}, []);
Anti-Patterns
❌ DON'T:
- Create objects inside useFrame
- Use high polygon models without LOD
- Skip dispose() calls
- Use shadows on mobile without testing
- Render invisible objects
- Use uncompressed textures
✅ DO:
- Reuse Vector3, Quaternion instances
- Implement LOD for complex scenes
- Always dispose geometries and materials
- Profile before and after optimizations
- Use Instances for repeated objects
- Compress textures (WebP, Basis)
Performance Monitoring
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
function PerformanceMonitor() {
const frameCount = useRef(0);
const lastTime = useRef(performance.now());
useFrame(() => {
frameCount.current++;
const now = performance.now();
if (now - lastTime.current >= 1000) {
console.log(`FPS: ${frameCount.current}`);
frameCount.current = 0;
lastTime.current = now;
}
});
return null;
}
Checklist
Performance review:
- DPR limited appropriately
- Instancing used for repeated objects
- LOD implemented for complex models
- Dispose called on cleanup
- No object creation in useFrame
- Shadows disabled on mobile
- Textures compressed
- Draw calls under budget
- FPS stable at 60
Common Performance Killers
- Too many draw calls → Use Instances
- High polygon count → Use LOD
- Unoptimized textures → Compress, resize
- No frustum culling → Enable frustumCulled
- Memory leaks → Call dispose()
- GC pressure → Object pooling
- Expensive shaders → Simplify, use mobile variants
- Post-processing → Limit on mobile
Related Skills
For R3F fundamentals: Skill("ta-r3f-fundamentals")
For material optimization: Skill("ta-r3f-materials")
External References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?