Boost FPS with Three.js InstancedMesh: A Practical Guide to Rendering 10,000 Boxes
Render 10,000 identical boxes in Three.js with a single draw call using InstancedMesh. Learn the performance gains, trade‑offs, and a step‑by‑step example to boost FPS in large scenes.
28 Feb 2026, 15:46 UTC

Concrete Problem: Rendering Thousands of Identical Objects
Suppose you’re building a forest scene for a web game. You need 10,000 trees, each a simple cylinder with a bark texture. If you create a separate Mesh for each tree, the renderer issues 10,000 draw calls per frame. On most consumer GPUs that drops the frame rate to 10–15 fps, making the scene feel sluggish.
Why InstancedMesh Solves It
The THREE.InstancedMesh class lets you render many copies of the same geometry and material with a single draw call. Internally it stores one geometry, one material, and an array of transformation matrices—one per instance. The GPU applies each matrix during rendering, so the CPU only sends a handful of data structures per frame.
Key benefits:
- From thousands of draw calls to just one.
- Minimal CPU overhead: only matrix updates when objects move.
- Reduced memory footprint because geometry and material are shared.
Worked Example: 10,000 Randomly Placed Boxes
Below is a minimal setup that demonstrates the performance difference. The code assumes Three.js r129 or newer.
// Setup scene, camera, renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 100);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Stats.js for FPS and draw‑call monitoring
const stats = new Stats();
document.body.appendChild(stats.dom);
// Geometry and material shared by all instances
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
// Create InstancedMesh with 10,000 copies
const count = 10000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
scene.add(instancedMesh);
// Create a temporary matrix for setting instance transforms
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * 200,
0,
(Math.random() - 0.5) * 200
);
dummy.scale.setScalar(Math.random() * 0.5 + 0.5);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
// Simple lighting
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 1, 1).normalize();
scene.add(light);
// Animation loop
function animate() {
requestAnimationFrame(animate);
stats.update();
renderer.render(scene, camera);
}
animate();
To compare, replace the InstancedMesh with 10,000 individual Mesh objects and observe the FPS drop in stats.js. The renderer info (renderer.info) will show draw‑call counts: 1 for the instanced version vs. 10,000 for the naive one.
Trade‑Offs and Limitations
- Per‑instance attributes: If you need each instance to have a unique color or texture, you must add an
InstancedBufferAttributeor write a custom shader. This adds complexity and may reduce the performance gain. - Dynamic updates: Updating the matrix of many instances each frame can become expensive. For fully dynamic scenes, consider grouping objects into multiple
InstancedMeshinstances or using a hybrid approach. - Memory usage: The matrix buffer occupies
count × 16 × 4bytes (for a 4×4 matrix of 32‑bit floats). For 10,000 instances that’s roughly 640 KB—small, but still a consideration on very low‑end devices. - GPU support: Instancing requires WebGL2 or a WebGL1 extension. Ensure the target browsers support
WEBGL_draw_instancedor use thethree/examples/jsm/webgl/WEBGL.jshelper to check.
Actionable Checklist for Your Project
- Profile the scene with
stats.js. If you see >200 draw calls for identical objects, InstancedMesh is a win. - Replace repeated
Meshobjects with a singleInstancedMeshwhere geometry and material are identical. - Assign per‑instance transforms once during initialization. Update only the matrices that actually change each frame.
- If you need per‑instance colors, add an
InstancedBufferAttributeand modify your shader to use it. Test the performance impact. - When the instanced objects are no longer needed, call
instancedMesh.dispose()and remove it from the scene to free GPU memory. - Use
renderer.info.render.callsandrenderer.info.render.trianglesto verify that the draw‑call count has dropped to 1 and that triangle count remains unchanged.
By following this pattern, you’ll keep your FPS high even with thousands of identical objects, making your web applications feel smooth and responsive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.