Three.js performance and implementation best-practice reference. Use when writing, reviewing, or optimising a Three.js application — covers memory disposal, render loop, draw calls, instancing, glTF asset loading, materials, lighting, shaders (GLSL and TSL), WebGPU, WebXR, mobile, and post-processing. Adapted from emalorenzo/three-agent-skills (MIT).
Instrucciones de origen · Vista previa de solo lectura
name
three-js-best-practices
description
Three.js performance and implementation best-practice reference. Use when writing, reviewing, or optimising a Three.js application — covers memory disposal, render loop, draw calls, instancing, glTF asset loading, materials, lighting, shaders (GLSL and TSL), WebGPU, WebXR, mobile, and post-processing. Adapted from emalorenzo/three-agent-skills (MIT).
license
MIT
compatibility
Portable reference skill for agents that support markdown skills or prompt files. Works best alongside project Three.js source files, Chrome DevTools Performance captures, and Spector.js GPU captures.
disable-model-invocation
true
metadata
{"owner":"game-delivery","version":"2.0.0","language":"en-GB","category":"web-rendering","upstream_references":["https://github.com/emalorenzo/three-agent-skills (MIT — see NOTICE.md)"],"tags":["three-js","webgl","webgpu","performance","shaders","tsl","gltf","memory","instancing","webxr","mobile"],"intents":["code-review","performance-optimisation","memory-audit","draw-call-reduction","shader-review","webgpu-migration","mobile-optimisation"],"output_types":["rule-citation","review-findings","optimisation-plan","code-correction"]}
Three.js Best Practices
Reference guide for Three.js r170+. Rules are grouped by impact category. Within each category, critical rules are marked CRITICAL.
1. Memory and Disposal
CRITICAL — Three.js does not automatically garbage-collect GPU resources. Removing a mesh from the scene does not free its GPU memory. Always call .dispose() explicitly.
Dispose geometry and material on removal
// BAD — GPU buffer leaks
scene.remove(mesh);
mesh = null;
// GOOD — explicit GPU cleanupfunctiondisposeMesh(mesh) {
scene.remove(mesh);
mesh.geometry.dispose();
if (Array.isArray(mesh.material)) {
mesh.material.forEach(m =>disposeMaterial(m));
} else {
disposeMaterial(mesh.material);
}
}
functiondisposeMaterial(material) {
material.dispose();
for (const key ofObject.keys(material)) {
const value = material[key];
if (value && typeof value.dispose === 'function') {
value.dispose(); // textures
}
}
}
CRITICAL — Use renderer.setAnimationLoop() instead of requestAnimationFrame when targeting WebXR. For standard projects either works, but setAnimationLoop is safer.
// GOOD
renderer.setAnimationLoop((timestamp, frame) => {
// frame is the XRFrame when in XR sessionupdate(timestamp);
renderer.render(scene, camera);
});
// Stop cleanly
renderer.setAnimationLoop(null);
Delta time for frame-rate-independent animation
CRITICAL — Always use delta time. Fixed increments run at different speeds on different devices.
const clock = newTHREE.Clock();
renderer.setAnimationLoop(() => {
const delta = clock.getDelta(); // seconds since last frameconst elapsed = clock.getElapsedTime(); // total seconds
mesh.rotation.y += 1.0 * delta; // 1 radian/second regardless of fps
renderer.render(scene, camera);
});
3. Draw Calls and Instancing
Target: fewer than 100 draw calls per frame on mobile. Each THREE.Mesh is one draw call.
Monitor: renderer.info.render.calls
InstancedMesh for identical objects
// BAD — 10,000 draw callsfor (let i = 0; i < 10000; i++) {
const mesh = newTHREE.Mesh(geometry, material);
scene.add(mesh);
}
// GOOD — 1 draw callconst instancedMesh = newTHREE.InstancedMesh(geometry, material, 10000);
const dummy = newTHREE.Object3D();
const color = newTHREE.Color();
for (let i = 0; i < 10000; i++) {
dummy.position.random().multiplyScalar(100);
dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
color.setHSL(Math.random(), 0.8, 0.5);
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceMatrix.needsUpdate = true;
if (instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true;
scene.add(instancedMesh);
When to use InstancedMesh: > 50 objects with the same geometry.
When NOT to use: objects need different geometries or different materials.
BatchedMesh for varied geometry sharing one material
Use object pooling to eliminate garbage collection pauses during gameplay. Critical for bullets, particles, enemies, and any frequently spawned/destroyed objects.
classObjectPool {
constructor(factory, initialSize = 50) {
this.pool = [];
this.active = newSet();
this.factory = factory;
for (let i = 0; i < initialSize; i++) this.pool.push(factory());
}
get() {
const obj = this.pool.pop() ?? this.factory();
this.active.add(obj);
obj.visible = true;
return obj;
}
release(obj) {
this.active.delete(obj);
obj.visible = false;
// Reset state here before returningthis.pool.push(obj);
}
}
// Pre-warm during loading (never during gameplay)const bulletPool = newObjectPool(() => {
const mesh = newTHREE.Mesh(bulletGeo, sharedMat); // shared geometry and material
scene.add(mesh);
mesh.visible = false;
return mesh;
}, 200);
Detail split out of this file. Each is self-contained; read one only when its trigger applies.
references/shaders.md — Shaders — GLSL and TSL. Read when writing, reviewing or optimising custom shader code, or migrating GLSL to TSL.
references/platform-targets.md — Platform targets — WebGPU, WebXR and mobile. Read when targeting the WebGPU renderer, building for VR/AR headsets, or tuning for mobile browsers.
references/post-processing.md — Post-processing. Read when adding bloom, depth of field, ambient occlusion, outlines or any screen-space effect.
Quick-reference checklist
All removed geometries, materials, and textures are disposed
Delta time used in all animation loops
renderer.setAnimationLoop() used (required for WebXR)
Draw calls < 100 on mobile target (check renderer.info.render.calls)
InstancedMesh used for > 50 identical objects
Pixel ratio capped (1.5 mobile, 2 desktop)
Textures are power-of-two and sized for target platform
Shadow map sized for platform (512 mobile, 2048 desktop)
Post-processing effects merged into fewest passes possible
glTF assets use Draco/Meshopt compression + KTX2 textures
Decoders (Draco, KTX2) hosted locally, not from CDN