Rendering Thousands of Identical Objects with Three.js InstancedMesh
Learn how Three.js InstancedMesh reduces draw calls from thousands to one when rendering large grids of identical objects, with a working code example, trade‑offs, and verification steps.
06 Apr 2026, 12:23 UTC

The problem: too many draw calls
When you need to show many copies of the same object—think a forest of trees, a field of grass, or a voxel grid—creating a separate Mesh for each copy forces the GPU to issue a draw call for every instance. In a scene with 10 000 cubes, that means 10 000 draw calls per frame, which quickly overwhelms the CPU and drops the frame rate.
Thesis: InstancedMesh collapses draw calls
Three.js provides InstancedMesh, which shares a single BufferGeometry and Material among all instances while storing a separate 4×4 transformation matrix for each one. The renderer can then issue a single draw call (or a few, depending on the extensions) and move the per‑instance work to the GPU, drastically reducing CPU overhead and memory usage.
When to use it
- All instances share the same geometry and material.
- You need to change position, rotation, or scale per instance.
- You are targeting WebGL2 or have the
ANGLE_instanced_arraysextension available.
Worked example: a 100×100 grid of cubes
The following snippet creates 10 000 identical cubes arranged in a grid. It assumes you have a scene, camera, and renderer already set up.
// 1. Shared geometry and material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });
// 2. Create the InstancedMesh – 10 000 instances
const count = 100 * 100;
const grid = new THREE.InstancedMesh(geometry, material, count);
scene.add(grid);
// 3. Set each instance’s matrix
const dummy = new THREE.Object3D(); // helper to compute matrices
let index = 0;
for (let x = -50; x < 50; x++) {
for (let z = -50; z < 50; z++) {
dummy.position.set(x, 0, z);
dummy.updateMatrix();
grid.setMatrixAt(index, dummy.matrix);
index++;
}
}
// 4. Tell Three.js the matrix buffer changed
grid.instanceMatrix.needsUpdate = true;
// 5. Animation loop – only update matrices that actually move
function animate() {
requestAnimationFrame(animate);
// Example: make a wave motion over time
const time = performance.now() * 0.001;
index = 0;
for (let x = -50; x < 50; x++) {
for (let z = -50; z < 50; z++) {
const height = Math.sin(x * 0.2 + time) * Math.cos(z * 0.2 + time) * 0.5;
dummy.position.set(x, height, z);
dummy.updateMatrix();
grid.setMatrixAt(index, dummy.matrix);
index++;
}
}
grid.instanceMatrix.needsUpdate = true; // crucial step
renderer.render(scene, camera);
}
animate();
Key points:
- The geometry and material are created once.
setMatrixAtwrites the transformation for each instance into the internal buffer.- After modifying the buffer, you must set
needsUpdate = trueoninstanceMatrix; otherwise the GPU sees stale data. - In the render loop you only update the matrices that changed; the draw call count stays at one.
Trade‑off: per‑instance variations
InstancedMesh excels when every instance shares the same geometry and material. If you need per‑instance attributes such as unique colors, texture offsets, or custom shader inputs, the built‑in InstancedMesh cannot store them directly. You have two options:
- Use
InstancedBufferGeometrywith a custom shader that reads additional per‑instance buffers (e.g., a color buffer). This keeps the single draw call but requires writing shader code. - Fall back to individual
Meshobjects for the few instances that need variation, accepting the extra draw calls for those objects.
Either approach adds complexity; evaluate whether the visual gain justifies the extra work.
Verification and practical checks
To confirm that the optimization is working:
- Open Chrome DevTools → Rendering → enable “Show layer borders” or WebGL inspector and watch the draw call count drop from thousands to one when you switch from individual meshes to
InstancedMesh. - Add a simple FPS counter (e.g.,
stats.js) and compare the frame rate before and after the change; a smoother frame rate indicates reduced CPU load. - Inspect the buffer size:
grid.instanceMatrix.array.lengthshould equalcount * 16(16 floats per 4×4 matrix). After updating matrices, verify that the modified range contains the expected values.
If you see stale transforms, double‑check that you set needsUpdate = true after each batch of matrix changes.
Limitations to keep in mind
- WebGL1 requires the
ANGLE_instanced_arraysextension; some older mobile browsers may not expose it or may limit the maximum instance count (often to a few thousand). Test on your target devices. - Updating a large number of matrices each frame still incurs CPU cost; only update those that actually change.
- Material changes (e.g., switching textures) cannot be done per instance without a custom shader.
Actionable closing
Start by identifying repetitive objects in your scene. Replace a batch of individual meshes with an InstancedMesh, set their matrices, and mark the buffer as needing an update. Verify the draw‑call drop with the browser’s WebGL inspector and monitor frame‑rate improvements. If you later need per‑instance data, consider migrating to InstancedBufferGeometry with a tailored shader, but keep the simple InstancedMesh path as your baseline for high‑performance rendering.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.