| name | three-js |
| description | [Applies to: **/*.{js,jsx}] Enforce modern, performant, and maintainable three-js development practices, focusing on WebGL error handling, efficient asset management, and render loop optimization. |
| source | cursor_mdc |
three-js Best Practices
This guide establishes the definitive coding standards for three-js development, ensuring robust, performant, and maintainable 3D applications. Adhere to these rules to prevent common pitfalls and leverage three-js effectively.
1. WebGL Error Handling & Extension Support
Always treat WebGL as a low-level layer requiring explicit error checks. Never assume universal extension support.
✅ GOOD: Proactive Error Checking & Graceful Fallbacks
During development, actively check gl.getError() to catch issues beyond OUT_OF_MEMORY or CONTEXT_LOST. In production, avoid blocking calls like getError() and getParameter(). Query extension support and provide fallbacks.
function checkGLErrors(gl) {
let error = gl.getError();
while (error !== gl.NO_ERROR) {
console.error(`WebGL Error: ${error}`);
error = gl.getError();
}
}
const renderer = new THREE.WebGLRenderer();
const gl = renderer.getContext();
if (gl.getExtension('ANGLE_instanced_arrays')) {
} else {
console.warn('ANGLE_instanced_arrays not supported, falling back to non-instanced rendering.');
}
❌ BAD: Ignoring WebGL Errors & Blindly Using Extensions
const geometry = new THREE.BufferGeometry();
renderer.render(scene, camera);
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
2. Asset Management & Lazy Loading
Optimize initial load times and prevent frame stalls by loading large assets lazily and using the correct loaders.
✅ GOOD: Lazy Loading with Dedicated Loaders & Modular Structure
Load assets only when needed. Use GLTFLoader, TextureLoader, etc., and organize assets in a public/ directory.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
export function loadModel(url, scene) {
const loader = new GLTFLoader();
return new Promise((resolve, reject) => {
loader.load(
url,
(gltf) => {
scene.add(gltf.scene);
resolve(gltf.scene);
},
(xhr) => {
console.log(`${(xhr.loaded / xhr.total) * 100}% loaded`);
},
(error) => {
console.error('An error happened loading the model:', error);
reject(error);
}
);
});
}
❌ BAD: Synchronous Loading & Unorganized Assets
const texture = new THREE.TextureLoader().load('large_texture.jpg');
const model = new GLTFLoader().load('heavy_model.glb');
scene.add(model.scene);
3. Performance Optimization
Minimize draw calls, reuse resources, and keep shaders efficient.
✅ GOOD: Instancing, Geometry Batching, Material Reuse, & Texture Atlases
For repetitive objects, use InstancedMesh. Combine static geometries where possible. Reuse materials across objects. Consolidate textures into atlases.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshNormalMaterial();
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50);
mesh.setMatrixAt(i, matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
const sharedMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const obj1 = .( .(), sharedMaterial);
obj2 = .( .(), sharedMaterial);
❌ BAD: Excessive Draw Calls & Unique Materials
for (let i = 0; i < 1000; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50);
scene.add(mesh);
}
4. Resource Disposal
Prevent memory leaks by explicitly disposing of three-js objects when they are no longer needed.
✅ GOOD: Explicit Disposal
Always call dispose() on geometries, materials, textures, and render targets when removing them from the scene.
function disposeObject(obj) {
if (obj.geometry) {
obj.geometry.dispose();
}
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(material => material.dispose());
} else {
obj.material.dispose();
}
}
if (obj.texture) {
obj.texture.dispose();
}
if (obj.parent) {
obj.parent.remove(obj);
}
}
const meshToRemove = scene.getObjectByName('myDynamicMesh');
if (meshToRemove) {
disposeObject(meshToRemove);
}
❌ BAD: Relying on Garbage Collection
const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());
scene.add(mesh);
scene.remove(mesh);
mesh = null;
5. Project Structure & Development Workflow
Adopt a modern development setup for efficient development and deployment.
✅ GOOD: npm + Vite & Modular Imports
Use npm for dependency management and Vite for local development and production builds. Structure your project with a clear entry point (main.js) and a public/ folder for static assets.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My three.js App</title>
</head>
<body>
<script type="module" src="/main.js"></script>
</body>
</html>
// main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ... rest of your scene setup
❌ BAD: CDN-only & Global Scope Pollution
<script src="https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.149.0/examples/jsm/controls/OrbitControls.js"></script>
<script>
const scene = new THREE.Scene();
</script>