Use when a Three.js scene has performance problems: low FPS, memory leaks, too many draw calls, or high GPU memory usage. Prevents the common mistake of forgetting dispose(), creating objects in the render loop, or not using InstancedMesh for repeated objects. Covers disposal patterns, draw call optimization, InstancedMesh, LOD, texture compression, renderer.info, Stats.js profiling. Keywords: performance, memory leak, dispose, draw calls, FPS, slow, optimization, InstancedMesh, LOD, renderer.info, Stats, profiling, laggy, low FPS, stuttering, browser freezes.
Use when a Three.js scene has performance problems: low FPS, memory leaks, too many draw calls, or high GPU memory usage. Prevents the common mistake of forgetting dispose(), creating objects in the render loop, or not using InstancedMesh for repeated objects. Covers disposal patterns, draw call optimization, InstancedMesh, LOD, texture compression, renderer.info, Stats.js profiling. Keywords: performance, memory leak, dispose, draw calls, FPS, slow, optimization, InstancedMesh, LOD, renderer.info, Stats, profiling, laggy, low FPS, stuttering, browser freezes.
license
MIT
compatibility
Designed for Claude Code. Requires Three.js r160+.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
threejs-errors-performance
Performance Diagnosis Flowchart
Scene is slow / low FPS
│
├─ Check renderer.info.render.calls
│ ├─ > 200 draw calls ──────────────── Go to: Draw Call Optimization
│ └─ < 200 draw calls
│
├─ Check renderer.info.memory
│ ├─ geometries/textures growing ──── Go to: Memory Leak Diagnosis
│ └─ stable counts
│
├─ Check renderer.info.render.triangles
│ ├─ > 2M triangles ────────────────── Go to: Geometry Optimization (LOD, merge)
│ └─ < 2M triangles
│
├─ Check GPU load (DevTools Performance tab)
│ ├─ GPU-bound (long GPU tasks) ───── Go to: Shader / Material Optimization
│ └─ CPU-bound (long JS tasks) ────── Go to: CPU Optimization
│
└─ Check Stats.js memory panel
├─ JS heap growing ─────────────── Go to: JavaScript Object Leaks
└─ Heap stable ──────────────────── Profile specific bottleneck
Quick Reference
renderer.info: Your First Diagnostic Tool
import * asTHREEfrom'three';
// ALWAYS check renderer.info when diagnosing performanceconsole.log(renderer.info.render);
// { calls: number, triangles: number, points: number, lines: number, frame: number }console.log(renderer.info.memory);
// { geometries: number, textures: number }console.log(renderer.info.programs);
// Array of compiled shader programs (length = unique material combinations)
Rule: If renderer.info.memory.geometries or renderer.info.memory.textures grows continuously over time, you have a memory leak. ALWAYS monitor these values during development.
Rule: NEVER merge geometries that need independent transforms, materials, or raycasting targets. Merging makes individual object interaction impossible.
LOD (Level of Detail)
import * asTHREEfrom'three';
const lod = newTHREE.LOD();
lod.addLevel(highDetailMesh, 0); // visible 0-50 units
lod.addLevel(mediumDetailMesh, 50); // visible 50-200 units
lod.addLevel(lowDetailMesh, 200); // visible 200+ units
scene.add(lod);
// ALWAYS call in animation loop for distance-based switching
lod.update(camera);
Rule: ALWAYS provide at least 3 LOD levels for objects visible across a wide distance range. Triangle counts MUST decrease by at least 50% between each level.
Texture Optimization
Technique
Impact
When to Use
Resize textures
High
ALWAYS use the smallest resolution that looks acceptable
Power-of-two dimensions
Medium
Required for mipmaps; ALWAYS use (256, 512, 1024, 2048)
Compressed formats (KTX2/Basis)
High
ALWAYS for production; 4-6x smaller GPU footprint
Texture atlases
High
Combine multiple small textures into one to reduce draw calls
texture.dispose() on swap
Critical
ALWAYS dispose old texture before assigning new one
generateMipmaps: false
Low
Use for UI textures or textures that NEVER need filtering at distance
Frustum culling is enabled by default (object.frustumCulled = true). The renderer skips objects outside the camera view.
NEVER disable frustum culling globally. Only set frustumCulled = false on specific objects that MUST render regardless of camera (skyboxes, large particle systems, full-screen post-processing quads).
Rule: For InstancedMesh, frustum culling operates on the entire instance group as one bounding sphere. If instances are spread across a large area, split them into spatial groups for effective culling.
Object Pooling
NEVER create and destroy objects every frame. Use object pools for frequently spawned/despawned objects:
For objects that NEVER move after initial placement:
object.matrixAutoUpdate = false;
object.updateMatrix(); // compute once
This prevents the renderer from recalculating the local matrix every frame for static objects.
Avoid Allocations in the Render Loop
// WRONG: creates new Vector3 every frame — GC pressurefunctionanimate() {
const pos = newTHREE.Vector3(1, 2, 3); // NEVER allocate in loop
mesh.position.copy(pos);
}
// CORRECT: reuse pre-allocated objectsconst _tempVec = newTHREE.Vector3();
functionanimate() {
_tempVec.set(1, 2, 3);
mesh.position.copy(_tempVec);
}
Rule: ALWAYS declare temporary math objects (Vector3, Matrix4, Quaternion, Color, Box3) outside the animation loop. Prefix with _ to indicate they are reusable scratch variables.
Shader and Material Optimization
Action
Impact
Use MeshStandardMaterial instead of MeshPhysicalMaterial
Fewer shader instructions unless you need clearcoat/transmission/sheen
Minimize unique material count
Fewer shader compilations; ALWAYS share materials across identical meshes
Set material.precision = 'mediump' on mobile
Faster fragment shading on mobile GPUs
Avoid onBeforeCompile unless necessary
Each unique modification creates a new shader variant
Chrome DevTools Profiling
Performance tab: Record a few seconds, look for long "GPU" tasks and JS frame duration
Memory tab: Take heap snapshots before and after scene changes to find unreleased objects
renderer.info logging: Add a periodic console.log(renderer.info.memory) to detect leaks
NEVER create BufferGeometry, Material, or Texture objects inside the render/animation loop. This causes memory to grow without bound.
NEVER call renderer.render() after renderer.dispose(). The WebGL context is destroyed.
NEVER dispose shared geometry/material/texture while other meshes still reference it. Track reference counts or dispose only when ALL consumers are removed.
ALWAYS dispose old textures before replacing: if (material.map) material.map.dispose(); material.map = newTexture;
ALWAYS call controls.dispose() when removing OrbitControls or other control instances. Failing to do so leaks DOM event listeners.
ALWAYS set instanceMatrix.needsUpdate = true after calling setMatrixAt() on an InstancedMesh. Without this, instances render at the origin.