Reduce Three.js Draw Calls in renderer.info.render.calls

In Three.js, rendering performance is heavily tied to the number of draw calls sent to the GPU. This article explains how to measure these draw calls using the renderer.info.render.calls property and outlines actionable techniques—such as geometry merging, instanced rendering, and material sharing—to significantly reduce them and boost your application’s frame rate.

Measuring Draw Calls with renderer.info

Three.js provides a built-in debugging tool via the WebGLRenderer.info object. This object monitors the performance of the renderer in real-time.

To measure the active draw calls in your scene, access the renderer.info.render.calls property inside your animation loop:

function animate() {
    requestAnimationFrame(animate);

    // Render the scene
    renderer.render(scene, camera);

    // Log the number of draw calls
    console.log("Draw Calls:", renderer.info.render.calls);
    console.log("Triangles:", renderer.info.render.triangles);
}

Each draw call represents a separate instruction sent by the CPU to the GPU to render a group of vertices. High draw call counts (typically over a few hundred or thousand, depending on the device) cause CPU bottlenecks, leading to drops in frame rates.


How to Reduce Draw Calls

To optimize your scene, you must minimize the number of individual render commands. Below are the most effective strategies to achieve this.

1. Use InstancedMesh for Duplicate Objects

If your scene contains many identical geometries that share the same material (such as trees, particles, or building bricks), use THREE.InstancedMesh. This allows you to render thousands of objects in a single draw call while still positioning, scaling, and rotating them individually.

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const count = 1000;

// Create an InstancedMesh
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
    dummy.position.set(Math.random() * 10, Math.random() * 10, Math.random() * 10);
    dummy.updateMatrix();
    instancedMesh.setMatrixAt(i, dummy.matrix);
}

scene.add(instancedMesh);
// Result: 1 draw call instead of 1000

2. Merge Geometries for Static Objects

If you have multiple unique static meshes that do not need to move independently, merge their geometries into a single mesh. You can use BufferGeometryUtils.mergeGeometries from the Three.js addons.

import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';

const geometries = [];

// Create multiple geometries
for (let i = 0; i < 100; i++) {
    const geom = new THREE.BoxGeometry(1, 1, 1);
    geom.translate(Math.random() * 10, 0, Math.random() * 10); // Position manually
    geometries.push(geom);
}

// Merge into a single geometry
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(mergedGeometry, material);

scene.add(mesh);
// Result: 1 draw call instead of 100

3. Share Materials and Use Texture Atlases

Three.js groups objects by material during the rendering process. If two meshes have the same geometry but different materials, they will require separate draw calls.

4. Enable Frustum Culling

By default, Three.js enables frustum culling (mesh.frustumCulled = true). This automatically skips rendering objects that are outside the camera’s field of view.

5. Combine Multi-Material Meshes

Using an array of materials on a single mesh (e.g., a cube with six different materials) forces Three.js to split the mesh into subgroups, resulting in one draw call per material. Whenever possible, replace multi-material meshes with a single material that utilizes a texture map or vertex colors.