--- name: threejs-perf-and-teardown description: three.js r185 essentials — 60fps budget, InstancedMesh over per-node Meshes, dispose everything on scene teardown. when_to_use: You're coding or profiling a three.js/WebGL/WebGPU scene. threeJS team pins. tags: [threejs, webgl, webgpu, performance, versioned] --- # three.js perf + teardown (r185) Anchored to **three.js r185** (2026-07). WebGPURenderer is stable enough for production on Chromium/Safari; WebGL2 renderer still the default fallback. ## Frame budget Target **16.6 ms/frame** (60 fps) on the median target device. Break it down: - JS + scene graph updates: ≤ 4 ms - GPU submission: ≤ 4 ms - GPU rendering (browser waits): remainder If you can't hit 60 fps at 1440p on a mid-range GPU, DON'T ship as-is. Downgrade features (fewer shadow casters, lower shadow map res, LOD swaps) before shipping. ## Draw call budget - Target **≤ 200 draw calls per frame**. Every distinct material × geometry combo = ≥ 1 draw call. - **InstancedMesh** for any repeated geometry. 10,000 trees = 1 draw call, not 10,000. - **BatchedMesh** (added in r168+, matured through r185) for heterogeneous geometry with shared material — batch different meshes with one call. ## Memory teardown **The #1 cause of "why does the tab crash after 20 minutes"**. GPU resources are NOT garbage collected — you must dispose. ```ts function teardown(scene: THREE.Scene) { scene.traverse((obj) => { if ((obj as THREE.Mesh).isMesh) { const m = obj as THREE.Mesh; m.geometry.dispose(); if (Array.isArray(m.material)) m.material.forEach((x) => x.dispose()); else m.material.dispose(); } }); renderer.dispose(); // WebGL context + programs renderer.forceContextLoss(); // guarantee GPU release, not deferred } ``` Textures must ALSO be disposed — walk materials and dispose any `map`, `normalMap`, `roughnessMap`, etc. ## Render-loop discipline - **Reuse math objects.** Never `new THREE.Vector3()` inside the render loop. ```ts const _tmp = new THREE.Vector3(); // module-level function tick() { _tmp.copy(a).sub(b).normalize(); // no allocation } ``` - Same for `Matrix4`, `Quaternion`, `Color`. - If you MUST allocate, batch outside the loop. ## Shadows - **One directional shadow-caster.** Bake everything else via lightmaps or ambient occlusion. - Shadow map resolution: 1024 for medium range, 2048 max. 4096 is a mobile-crash-in-a-can. - `light.shadow.autoUpdate = false; light.shadow.needsUpdate = true;` when the shadow is static — one render + freeze. ## Custom shaders - Try `ShaderMaterial` when built-ins get close but not exact. - `onBeforeCompile` hook for tweaking a built-in when it's 95% right — patch the shader source instead of rewriting. - WebGPU: WGSL, not GLSL. Migrate shader-by-shader as you adopt WebGPURenderer. ## Profiling - **Chrome DevTools Performance panel** — JS time. - **SpectorJS** browser extension — per-frame draw call breakdown, texture inspector, shader debugger. - **WebGL / WebGPU inspector** in Firefox — similar to Spector. ## Anti-patterns - **`new THREE.Mesh(geometry, material)` inside `requestAnimationFrame`.** Creates 60 mesh objects per second; profile-visible before you notice. - **Adding then removing lights on interaction.** Recompiles shaders (cache-miss). Toggle intensity to 0 instead. - **`renderer.setPixelRatio(window.devicePixelRatio)` on a 3× retina display without a quality slider.** Renders 9× the pixels; kills fps. Cap at 2 by default.