Golden rules
Draw calls < 100 per frame. Triangle count matters less than draw call count; beyond 500 draw calls, even a powerful GPU struggles. Use renderer.info.render.calls monitoring (for the full monitoring code, see references/examples.md)。
Diagnostic decision tree
| Symptom | Check first | Go deeper |
|---|---|---|
| Dropped frames, stuttering | renderer.info.render.calls Is it >100 |
Instancing/batching (below), post-processing chain |
| Memory keeps growing | renderer.info.memory Are the geometries/textures counts only increasing, never decreasing |
dispose iron rule (below),references/examples.md full cleanup code |
| Slow loading, VRAM blowing up | Are models uncompressed | references/assets.md Compression pipeline |
| Particle/physics bottleneck | Are CPU particles >50,000 | WebGPU compute shaders,references/webgpu.md |
| R3F project re-rendering inexplicably | Is there setState inside useFrame | R3F three rules (below) |
Quick reference of rules by area
WebGPU: when to migrate
Draw-call-heavy dropped frames / need compute shaders for physics particles (CPU limit around 50,000, GPU can reach millions) / complex post-processing chain stuttering.
WebGPURenderer must await renderer.init(). TSL compiles once to both WGSL/GLSL automatically.
Browser support baseline (data at time of recording, check caniuse for current status): Chrome/Edge 113+, Firefox 141+, Safari 26+.
Initialization fallback, full TSL guide, compute shader examples:references/webgpu.md。
Draw call optimization
- Large numbers ofidenticalgeometries (trees, rocks) →
InstancedMesh: 1,000 trees = 1 draw - Multipledifferentgeometries sharing a material →
BatchedMesh - Static small objects →
mergeGeometriesmerge - Materialsharing and reuse, never in a loop
new Material
Asset compression (benefit numbers)
- Draco geometry compression: 90-95% size reduction
- KTX2 textures: GPU memory reduced by about 10x
- One command:
gltf-transform optimize model.glb out.glb --texture-compress ktx2 --compress draco - Decoder path configuration and Meshopt/Draco selection:
references/assets.md
Memory iron rules
Three.js does not automatically reclaim GPU resources. Removing an object requires: geometry.dispose() + traversing all texture properties of the material and disposing each one + material.dispose(); GLTF's ImageBitmap also needs texture.source.data.close(). Use an object pool for frequently added/removed objects. Full code:references/examples.md。
Three shader rules
① Mobile precision mediump float(about 2x faster) ② Use mix/step instead of if branches (branches break GPU parallelism) ③ Pack data into vec4 to fetch 4 values per texture lookup.
Lighting and shadow budget
Active lights ≤3; PointLight shadows = 6 shadow map renders per light; texture size 512-1024 on mobile, 1024-2048 on desktop; static scenes shadowMap.autoUpdate = false trigger manually + bake lighting.
React Three Fiber Three Rules
① Animation goes through useFrame direct ref modification,never setState inside useFrame ② Static scenes use frameloop="demand" + invalidate() on-demand rendering ③ Toggle visibility via visible property, do not conditionally mount {show && <Model/>}(remounting rebuilds resources). Complete optimization template:references/examples.md。
Post-processing selection
WebGL → pmndrs/postprocessing(merge multiple effects with EffectPass); WebGPU → native TSL post-processing pipeline. Full setup for both:references/examples.md。
Acceptance criteria (self-check before completing an optimization task)
-
renderer.info.render.calls< 100 (exceeding requires giving a scene rationale) -
renderer.info.memorycount falls back after adding/removing objects, no monotonic growth - target device measured frame rate meets the target (test on mobile too, not just desktop)
- published assets go through the Draco/KTX2 compression pipeline
- every optimization suggestion corresponds to a measured symptom of the user's scene, not a checklist-style blanket application
What not to do
- General performance issues at the React component layer (memo/useMemo/bundle) →
vercel-react-best-practices - Three.js basics teaching, scene setup introduction
- Model/texture art production itself (only handles loading and rendering performance)
- Optimizing before measuring: when there is no renderer.info or frame rate data, install monitoring first before starting
Known pitfalls
| Pitfall | Specific manifestation | Countermeasure |
|---|---|---|
| Incomplete dispose | Only the geometry is disposed, all textures attached to materials leak | Traverse material properties and dispose each one; GLTF ImageBitmap also needs close() |
| setState inside useFrame | Triggers a React re-render every frame, frame rate plummets | Modify refs directly; only update state in interaction events |
| Switching models via conditional mount | {show && <Model/>} geometry and textures are rebuilt every time |
Toggle the visible property |
| Forgetting invalidate in demand mode | The camera moves but the picture does not, mistaken for "frozen" | Call invalidate() in the interaction callback |
| new Material inside a loop | 1000 meshes with 1000 materials, batching completely fails | Hoist the material outside the loop and share it |
| Casually adding shadows to PointLight | One point light does 6 shadow renders, mobile gives up immediately | Prefer SpotLight/DirectionalLight shadows, keep only one point light shadow |
Debugging tools
stats-gl(FPS/CPU/GPU)· lil-gui(real-time parameter tuning) · Spector.js(WebGL frame capture) · three-mesh-bvh(80k+ faces @60fps raycasting) · r3f-perf(R3F monitoring)
Reference docs (load on demand)
| File | When to read |
|---|---|
references/webgpu.md |
When migrating to WebGPU / writing TSL / compute shaders |
references/assets.md |
When configuring the compression pipeline, LOD, progressive loading |
references/examples.md |
When you need complete paste-ready code (monitoring/memory cleanup/object pooling/particles/R3F templates/post-processing/context loss recovery) |