The draw call problem
Every time Three.js renders a Mesh, it issues a draw call to the GPU. A draw call is an instruction that says "here's some geometry and a material, now be a good boy and go render it." Each draw call carries a bit of overhead, meaning that the CPU has to go and do a bunch of stuff like set up state, bind textures, upload uniforms, and communicate with the GPU driver. For a handful of objects this is invisible. For hundreds or thousands, it becomes a bottleneck.
If you need to render 1,000 identical cubes, like a field of crates or a particle cloud, creating 1,000 Mesh objects means 1,000 draw calls per frame. The GPU itself could handle the geometry easily, but the CPU spends most of its time just telling the GPU what to draw. The frame rate drops not because the scene is geometrically complex, but because the communication overhead is too high.
InstancedMesh solves this. It takes a single geometry and a single material, and draws them many times in one draw call. Each copy, called an instance, gets its own transformation matrix (position, rotation, scale), and optionally its own color. The GPU handles the repetition internally, which is what GPUs are built to do.
How InstancedMesh works
The constructor takes three arguments: a geometry, a material, and the number of instances.
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshStandardMaterial({ color: 0x4488cc });
// Create 1000 instances sharing this geometry and material
const mesh = new THREE.InstancedMesh(geometry, material, 1000);
scene.add(mesh);After creating the InstancedMesh, you need to position each instance. Every instance has a 4x4 transformation matrix that you set with setMatrixAt(index, matrix). You build the matrix using THREE.Matrix4, typically by composing a position, rotation, and scale with matrix.compose(position, quaternion, scale), or by using simpler helpers like matrix.setPosition() for position-only transforms.
const dummy = new THREE.Object3D();
for (let i = 0; i < 1000; i++) {
// Position each instance
dummy.position.set(
(i % 20) * 1.2 - 12, // x: grid column
0, // y: flat on ground
Math.floor(i / 20) * 1.2 - 30 // z: grid row
);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
// Tell Three.js the instance matrices have been set
mesh.instanceMatrix.needsUpdate = true;The Object3D trick is a common pattern. Instead of manually building a Matrix4, you use a temporary Object3D (which has familiar position, rotation, and scale properties), call updateMatrix() to compute its matrix, and then copy that matrix into the instance buffer.
A grid of cubes
The scene below renders 400 cubes in a 20x20 grid using a single InstancedMesh. There is just one draw call for the entire grid!
const gridSize = 20;
const count = gridSize * gridSize;
const spacing = 1.2;
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshStandardMaterial({
color: 0x4488cc,
roughness: 0.6,
metalness: 0.3
});
const mesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
const col = i % gridSize;
const row = Math.floor(i / gridSize);
dummy.position.set(
col * spacing - (gridSize * spacing) / 2,
0,
row * spacing - (gridSize * spacing) / 2
);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);Per-instance color
By default, all instances share the material's color. But InstancedMesh supports per-instance colors through the setColorAt() method. When you call this, Three.js creates an instanceColor buffer attribute that overrides the material's base color for each instance.
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
// Create a gradient based on position
const col = i % gridSize;
const row = Math.floor(i / gridSize);
const hue = (col + row) / (gridSize * 2);
color.setHSL(hue, 0.7, 0.5);
mesh.setColorAt(i, color);
}
// Tell Three.js the instance colors have been set
mesh.instanceColor.needsUpdate = true;The scene below uses a gradient hue across the grid. Each cube has a unique color, but they're all still rendered in a single draw call.
Animating instances
To animate instances, you update their matrices each frame and set instanceMatrix.needsUpdate = true. This tells Three.js to re-upload the matrix buffer to the GPU. The pattern is the same as initial setup, you loop through instances, update the dummy's transform, and then copy the matrix back.
The scene below animates a 30x30 grid of cubes in a sine wave. Each frame, every instance's Y position and rotation are recalculated based on its grid position and the current time.
const clock = new THREE.Clock();
function animate() {
const time = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
const col = i % gridSize;
const row = Math.floor(i / gridSize);
// Sine wave based on position and time
const y = Math.sin(col * 0.4 + time * 2) *
Math.cos(row * 0.4 + time * 2) * 1.5;
dummy.position.set(
col * spacing - offset,
y,
row * spacing - offset
);
dummy.rotation.y = time + col * 0.1;
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
// Critical: mark the buffer as needing re-upload
mesh.instanceMatrix.needsUpdate = true;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}Updating matrices every frame is more expensive than a static scene, but still far cheaper than managing thousands of individual Mesh objects. The GPU does the heavy lifting of actually rendering the instances, meaning that you're only paying the CPU cost of computing the transforms. Super cool!
When to use InstancedMesh
InstancedMesh is the right tool whenever you have many copies of the same geometry and material. Common use cases include:
- Vegetation: Thousands of grass blades, trees, or bushes across a terrain.
- Particles: Debris, sparks, snowflakes, or any volumetric effect where each particle is a small mesh rather than a point sprite.
- Architecture: Repeated structural elements like windows, bricks, columns, or floor tiles.
- Crowds: Simplified character models populating a city or stadium.
- Data visualization: Bar charts, scatter plots, or node graphs with hundreds of data points.
The rule of thumb is that if you're adding the same geometry to the scene more than a few dozen times, instancing will give you a measurable performance improvement. At hundreds or thousands of copies, it's the difference between 60 fps and single digits.
The limitations are straightforward. All instances must share the same geometry and material. If you need different geometries or fundamentally different materials, you'll need separate InstancedMesh objects (one per geometry/material combination). Per-instance color is supported, but per-instance textures or material properties beyond color require custom shaders.