Why and How to Use Three.js InstancedMesh for Thousands of Identical Objects
InstancedMesh lets Three.js render thousands of identical objects with a single draw call. This post explains the problem, shows a step‑by‑step example, discusses trade‑offs, and gives practical checks to confirm the performance gain.
07 Jun 2026, 15:31 UTC

Problem: Rendering Millions of Identical Objects
When a scene contains thousands of the same geometry – think trees, rocks, or a crowd of people – the naive approach is to create a Mesh for each object:
for (let i = 0; i < 10000; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
scene.add(mesh);
}
Every Mesh generates its own draw call, so a 10,000‑object scene forces the GPU to issue 10,000 separate commands. The CPU also has to maintain a large array of objects, and the driver must upload a new set of vertex buffers for each call. The result is a dramatic drop in frames per second (FPS) and a high memory footprint.
Thesis: InstancedMesh Batches All the Work
The THREE.InstancedMesh class solves this by keeping a single copy of the geometry and material while allowing each instance to have its own world matrix and per‑instance attributes. Internally, the renderer packs all instances into a single draw call, dramatically reducing CPU‑to‑GPU traffic and memory usage. The trade‑off is that all instances must share the same geometry and material, and the technique requires a WebGL2 context.
Implementation Steps
1. Verify WebGL2 Support
Three.js will silently fall back to a non‑instanced path on WebGL1. Explicitly check the renderer’s context before creating an InstancedMesh:
const renderer = new THREE.WebGLRenderer();
const gl = renderer.getContext();
const isWebGL2 = gl instanceof WebGL2RenderingContext;
if (!isWebGL2) {
console.warn('WebGL2 not available – InstancedMesh will not work.');
}
2. Create the InstancedMesh
Instantiate once with the geometry, material, and the total number of instances:
const count = 10000;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
scene.add(instancedMesh);
3. Set Per‑Instance Transforms
Each instance’s world matrix is set via setMatrixAt. The matrix can be built from a THREE.Matrix4 that encodes translation, rotation, and scale. After updating matrices, call instanceMatrix.needsUpdate = true:
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(Math.random() * 100, 0, Math.random() * 100);
dummy.rotation.y = Math.random() * Math.PI * 2;
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
4. Optional – Per‑Instance Color
To vary color per instance, enable the instanceColor attribute and set a Color for each instance:
instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3);
for (let i = 0; i < count; i++) {
const color = new THREE.Color(Math.random(), Math.random(), Math.random());
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceColor.needsUpdate = true;
Worked Example: 10,000 Moving Cubes
Below is a minimal, copy‑pasteable snippet that creates 10,000 cubes, animates them with a sine wave, and logs the FPS. Replace the InstancedMesh block with the naive loop to see the performance difference.
// Setup renderer, scene, camera
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 100);
// Light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 50);
scene.add(light);
// InstancedMesh
const count = 10000;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const mesh = new THREE.InstancedMesh(geometry, material, count);
scene.add(mesh);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set((i % 100) * 1.5, 0, Math.floor(i / 100) * 1.5);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
// Animation loop
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
dummy.position.y = Math.sin(t + i * 0.01) * 2;
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
Trade‑Offs & Limitations
- WebGL2 Requirement: InstancedMesh only works on browsers that expose a WebGL2 context. On older devices, you must fall back to manual batching or use a polyfill.
- Uniform Geometry/Material: All instances share the same geometry and material. If you need per‑instance textures or different shaders, InstancedMesh is not suitable.
- Matrix Update Overhead: Updating matrices every frame can become a CPU bottleneck when the instance count is extremely high (tens of thousands). Profile the CPU time spent in the animation loop.
- Debugging Complexity: Since the renderer sends a single draw call, you may not see individual objects in the WebGL debug panel. Use
instancedMesh.countandinstancedMesh.instanceMatrixto verify that instances exist.
Verification Checklist
- Open the browser console and run
renderer.getContext().getParameter(renderer.getContext().VERSION). It should return'WebGL 2.0'. - Open the WebGL debugging panel (e.g., Chrome DevTools > Performance > WebGL) and confirm that the number of draw calls drops from
countto1after enablingInstancedMesh. - Measure FPS before and after switching to
InstancedMesh. A noticeable increase (often 2–5×) indicates the batching is effective. - Inspect
mesh.instanceMatrixlength to ensure it matchescount * 16(each matrix has 16 floats).
Conclusion: When to Use InstancedMesh
If your scene contains thousands of the same geometry, THREE.InstancedMesh is the most efficient way to render them. It reduces GPU draw calls, saves memory, and keeps the CPU load manageable as long as you keep the instance count within a few tens of thousands and the transformations are relatively simple. Always verify that the browser supports WebGL2, and profile both CPU and GPU usage to confirm the expected performance boost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.