← All columns
THREE.JS2026.07.112 min read · demo

Cutting draw calls 200× with InstancedMesh

The standard move for repeating geometry — attribute layout and the frustum-culling trap.

Placing the same shape 200 times as 200 Mesh objects costs 200 draw calls. InstancedMesh does it in one.

const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial();
const mesh = new THREE.InstancedMesh(geo, mat, 200);

const m = new THREE.Matrix4();
for (let i = 0; i < 200; i++) {
  m.setPosition(Math.random() * 20 - 10, 0, Math.random() * 20 - 10);
  mesh.setMatrixAt(i, m);
}
scene.add(mesh);

Always raise the flag when updating

Rewriting instance matrices per frame does nothing on its own.

mesh.instanceMatrix.needsUpdate = true;

Forget it and everything freezes at its initial placement. No error is thrown, so when things “should be moving but aren’t”, check here first.

If every instance moves every frame, setting instanceMatrix usage to THREE.DynamicDrawUsage shaves a little off the upload.

Per-instance colour

Use setColorAt when instances need different colours.

const c = new THREE.Color();
mesh.setColorAt(i, c.setHSL(i / 200, 0.6, 0.5));
mesh.instanceColor.needsUpdate = true;

The material stays single; only colour rides along as an instance attribute. Anything richer — a per-instance lifetime, say — means adding your own InstancedBufferAttribute and patching the shader.

The frustum-culling trap

This is what cost me the most time. InstancedMesh culls against the whole mesh’s bounding sphere, not per instance.

Scatter instances across a wide area and that sphere becomes enormous, so the mesh is effectively always “on screen” and culling buys nothing. Worse, move instances far away without updating the sphere and they vanish while still visible.

Two fixes:

// 1. recompute after moving things
mesh.computeBoundingSphere();

// 2. or just turn culling off, if the mesh spans the view anyway
mesh.frustumCulled = false;

For objects spread across wide terrain, splitting into several InstancedMesh chunks and letting each chunk cull turned out to be the cleanest answer.

How much it helped

In the voxel demo, 10k cubes as individual meshes ran at roughly 40ms per frame. As a single InstancedMesh, around 3ms. The draw-call reduction shows up directly.