The Problem: Draw Calls
When you create 10,000 Mesh objects in Three.js and add
them to the scene, each one generates a
separate draw call when rendered. A draw call is a
CPU command sent to the GPU that says: bind this buffer, apply this
transform matrix, draw this geometry.
The GPU can process millions of polygons per second — but it can only process a few thousand draw call state changes per second. The CPU bottleneck kills your frame rate long before you run out of geometric complexity.
| N Objects | Method | Draw Calls | FPS |
|---|---|---|---|
| 1,000 | Mesh ×1000 | 1,000 | 55 |
| 10,000 | Mesh ×10000 | 10,000 | 12 |
| 100,000 | Mesh ×100000 | 100,000 | 1 |
| 100,000 | InstancedMesh | 1 | 60 |
The Solution: InstancedMesh
InstancedMesh packs all instance transforms into a single
GPU buffer and renders everything in one draw call. Each instance can
have an independent position, rotation, scale and colour — but they
all share one geometry and one material.
// Before: 10,000 draw calls
for (let i = 0; i < 10000; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(Math.random() * 100, 0, Math.random() * 100);
scene.add(mesh);
}
// After: 1 draw call
const instancedMesh = new THREE.InstancedMesh(geometry, material, 10000);
const dummy = new THREE.Object3D();
for (let i = 0; i < 10000; i++) {
dummy.position.set(Math.random() * 100, 0, Math.random() * 100);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);
Updating Instance Transforms Each Frame
For animated instances, update the matrix each frame and flag it dirty:
function animate() {
for (let i = 0; i < COUNT; i++) {
dummy.position.x = positions[i].x + Math.sin(time + i) * 0.1;
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true; // crucial!
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Gotcha: Forgetting
needsUpdate = true means the GPU never sees the new
matrices. Instances will appear frozen even though your JavaScript
loop runs. This is the single most common bug with InstancedMesh.
Per-Instance Colour
const color = new THREE.Color();
instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(
new Float32Array(COUNT * 3), 3
);
for (let i = 0; i < COUNT; i++) {
color.setHSL(i / COUNT, 1.0, 0.5);
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceColor.needsUpdate = true;
When NOT to Use InstancedMesh
- When instances need different materials (shaders) — use multiple InstancedMeshes, one per material
-
When you need per-instance raycasting with high precision —
instancedMesh.computeBoundingSphere()helps, but it batches the bounds - When the count changes every frame — prefer a pool approach and hide unused instances off-screen