Optimizing Three.js Performance with InstancedMesh
Thousands of individual meshes can stall your Three.js scene on draw calls alone. InstancedMesh renders them all in one call—here is how to set it up, update it, and know when not to use it.
08 Dec 2025, 04:00 UTC

When you build a 3D scene containing thousands of individual objects—like a forest of trees, a crowd, or an asteroid field—the primary bottleneck isn't usually the complexity of the polygons. Instead, it is the draw calls. Every time Three.js tells the GPU to render a mesh, there is a communication overhead cost. If you have 10,000 separate meshes, the CPU must manage 10,000 commands, which quickly leads to frame drops and stuttering performance.
The solution is InstancedMesh. This feature allows you to render thousands of copies of the same geometry and material in a single draw call. Each instance can maintain its own position, rotation, scale, and color without the heavy cost of individual scene graph objects.
The Draw Call Problem
In a standard Three.js setup, every Mesh is a unique node in the scene graph. The renderer must iterate through every node, calculate its world matrix, and send that data to the GPU. Even if the geometry is simple, the CPU becomes the bottleneck because it cannot keep up with the sheer volume of instructions.
InstancedMesh solves this by sending the geometry and material to the GPU once, followed by an array of transformation matrices. Each matrix defines where one specific instance should be placed. The GPU then handles the repetition internally, which is significantly faster than processing thousands of individual objects.
Implementing InstancedMesh
To use InstancedMesh, you must define the geometry, material, and the maximum number of instances you plan to use at creation time. This count is fixed because the GPU allocates the memory for the matrices upfront. The example below assumes a recent Three.js release (r150 or later) imported as a module.
Here is a practical example of setting up 1,000 cubes distributed randomly in space:
// 1. Setup geometry and material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });
const count = 1000;
// 2. Create the InstancedMesh
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
// 3. Use a dummy object to calculate transformations
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(
Math.random() * 50 - 25,
Math.random() * 50 - 25,
Math.random() * 50 - 25
);
dummy.rotation.y = Math.random() * Math.PI;
dummy.scale.setScalar(Math.random() + 0.5);
dummy.updateMatrix();
// Apply the matrix to the specific instance index
instancedMesh.setMatrixAt(i, dummy.matrix);
// Optional: per-instance color
const color = new THREE.Color(Math.random(), 0.5, 1.0);
instancedMesh.setColorAt(i, color);
}
// 4. Tell Three.js the data is ready
instancedMesh.instanceMatrix.needsUpdate = true;
if (instancedMesh.instanceColor) {
instancedMesh.instanceColor.needsUpdate = true;
}
scene.add(instancedMesh);The Object3D dummy is a common pattern: it gives you convenient position, rotation, and scale properties, and its updateMatrix() call composes those into the 4x4 matrix that setMatrixAt expects.
Handling Dynamic Updates
If your instances need to move (for example, floating debris), you must update their matrices in your animation loop. Because the data is stored in a buffer attribute, you must set the needsUpdate flag to true every time you modify the matrices so the new data is uploaded to the GPU.
function animate() {
requestAnimationFrame(animate);
for (let i = 0; i < count; i++) {
instancedMesh.getMatrixAt(i, dummy.matrix);
dummy.matrix.decompose(dummy.position, dummy.quaternion, dummy.scale);
dummy.position.y += 0.01;
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
renderer.render(scene, camera);
}Run this code in the browser as part of your Three.js application; no special permissions are needed. To verify the benefit, attach stats.js (or the browser's frame timing tools) and compare the same scene built with 1,000 individual Mesh objects against the instanced version. You should see fewer draw calls in renderer.info.render.calls—one for the instanced mesh instead of one per object.
Trade-offs and Limitations
Instancing is not a universal fix. Keep these constraints in mind:
- Shared geometry and material: every instance in one
InstancedMeshuses the same geometry and material. If you need different materials, create a separateInstancedMeshper material. - No per-vertex animation: morph targets and skinned animation are not supported per instance. Objects that need skeletal or vertex-level animation still require individual meshes.
- Fixed capacity: the instance count is set at construction. Growing beyond it means recreating the mesh or allocating extra headroom up front.
- CPU update cost: rewriting thousands of matrices every frame on the CPU can itself become a bottleneck. For very large dynamic sets, consider updating only the instances that changed, or moving transformations into a custom shader.
- Per-instance picking: raycasting works, but you must read the
instanceIdfrom the intersection result rather than getting a distinct object per hit.
Action Plan
If your scene renders many copies of the same object, profile first: check renderer.info.render.calls and your frame rate. If draw calls are high and the objects share geometry and material, convert them to a single InstancedMesh, manage transforms with a dummy Object3D, and set instanceMatrix.needsUpdate after changes. Measure again with identical lighting and camera settings to confirm the gain before committing to the refactor.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.