Efficient Rendering with InstancedMesh in Three.js
Learn how to replace many individual Three.js meshes with a single InstancedMesh to cut draw calls, improve FPS, and handle per‑instance color, with a practical example and verification steps.
23 Jul 2025, 08:33 UTC

Problem: Too Many Draw Calls
When a scene contains hundreds or thousands of identical objects—trees, bolts, tiled floors—each THREE.Mesh creates its own draw call. A draw call is a command to the GPU to render a set of geometry and material. If the GPU must process one call per object, the CPU becomes the bottleneck, especially on mobile devices or integrated graphics. The result is lower frame rates and higher power consumption.
Thesis: Use InstancedMesh for Shared Geometry
The thesis is simple: if the objects share the same BufferGeometry and Material and only differ by their transform (position, rotation, scale), group them into a single THREE.InstancedMesh. This reduces the number of draw calls from N to 1, while keeping the total triangle count unchanged.
How InstancedMesh Works
An InstancedMesh stores one copy of the geometry and material. For each instance you provide a Matrix4 that defines its local transform. The GPU draws all instances in one call, applying the matrix per vertex. This is why the CPU work drops dramatically.
Worked Example
// Assume you have a geometry and a material ready
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x88ccff });
// Create an InstancedMesh for 500 instances
const instanced = new THREE.InstancedMesh(
geometry,
material,
500 // count – number of instances
);
scene.add(instanced);
// Loop to set each instance's matrix
for (let i = 0; i < 500; i++) {
const matrix = new THREE.Matrix4();
// Random position within a 10x10 area
matrix.makeTranslation(
Math.random() * 10 - 5,
Math.random() * 10 - 5,
Math.random() * 10 - 5
);
// Optional: random scale
matrix.scale(
0.5 + Math.random(),
0.5 + Math.random(),
0.5 + Math.random()
);
instanced.setMatrixAt(i, matrix);
}
instanced.instanceMatrix.needsUpdate = true; // tell three.js to upload the matrices
Explanation of the code:
new THREE.InstancedMesh(geometry, material, count)creates the batch. Thecountplaceholder is the number of instances you intend to render.- The
forloop builds aMatrix4for each instance. You can compute position, rotation, and scale as needed. setMatrixAt(i, matrix)stores the matrix in the internal list. After the loop,instanceMatrix.needsUpdate = trueforces three.js to upload the updated matrices to the GPU.
Verification Steps
- Render them as individual
Meshobjects. In the browser console runrenderer.info.render.callsbefore and after a frame. You should see a high call count (close to 500). - Render them as a single
InstancedMesh. The samerenderer.info.render.callsvalue should drop dramatically, ideally to 1 or a few calls. - Measure frame time with the dev tools’ performance panel. The instanced version should show a lower render time.
- Test frustum culling: move the camera far away from the instanced group. If distant instances still appear, split the world into spatial chunks (e.g., multiple InstancedMesh objects) and repeat the call‑count measurement.
- For per‑instance color, set
instanced.instanceColor(or a customInstancedBufferAttribute) and verify that the material respects vertex colors in the current Three.js version.
Trade‑offs and Limitations
- Culling: The whole InstancedMesh is treated as one bounding volume. If you spread instances across a huge area, many may lie outside the camera frustum yet still be culled together, wasting GPU work. The practical fix is to divide the world into regions and create separate InstancedMesh objects per region.
- Per‑instance variation: Simple tinting works via
instanceColor, but complex per‑instance data (e.g., different shaders, textures) requires custom shader attributes orInstancedBufferAttribute, which adds development complexity. - GPU load: Instancing reduces CPU overhead but does not reduce the total number of vertices or triangles. Extremely dense geometry can still saturate the GPU.
- Raycasting: Picking a specific instance requires handling
instanceIdand can be more expensive than raycasting against individual meshes, especially with very large counts. - Dynamic updates: If you need to change each instance’s matrix every frame, you must rewrite the matrix array and set
needsUpdate = trueeach tick. For fully independent behavior, separate meshes may be simpler.
Practical Recommendation
- Keep distinct
Meshobjects for unique “hero” items that need individual material or animation changes. - Use a single
InstancedMeshfor static or semi‑static repeated sets (trees, bricks, particles). - Chunk large worlds: create multiple InstancedMesh instances, each covering a region that fits comfortably within a frustum. This restores effective culling and keeps the number of instances per batch manageable.
- When adding per‑instance color, test on your target Three.js version (e.g., r150) to ensure the material’s color handling matches expectations.
Finally, always verify the change in your own scene. Run the draw‑call comparison, watch frame time, and adjust chunking if culling appears ineffective. With these steps you can turn a draw‑call bound scene into a smooth, GPU‑efficient experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.