Optimizing Three.js: Handling Draw Calls, Instancing and Batching

When building a Three.js scene, we often get carried away. We add new meshes, make the geometry more complex, and import heavy models from Blender. At some point, even without any obvious complex logic or heavy generative calculations, we hit a wall. The FPS drops, CPU ms spikes, and the scene starts to lag. This problem pops up a lot when we add geometry in a loop, like generating map tiles. It also happens when we import large, unoptimized scenes from a 3D editor where every single screw is a separate object.

Posted On

September 20, 2026

Time To Read

5 mins

Identifying the Bottleneck

You can tank WebGL performance in many ways. You might be using heavy shaders, massive textures, or too many lights. But here, we will look at one of the most common causes of lag, which is a high number of draw calls.

To figure out that you have hit the limits of the engine and your hardware, you do not need to guess. Familiar tools like stats.js, the Performance tab in DevTools, or extensions like Scene Inspector show a pretty clear picture.

Once we start seeing more than 300 draw calls in the monitor, average laptops, office PCs, and phones usually start running at their absolute limit. Then, the FPS drops.

Key Metrics to Watch

If you open the performance monitor, you will see a few key metrics. Modern graphics cards can easily chew through millions of triangles, so the bottleneck is usually the draw calls, not the polycount. 

MetricTargetWarning ZoneDescription
FPS60Below 30Frames per second. Drops cause obvious lag.
MSUnder 16.620 to 30+CPU time per frame. High ms kills FPS.
CallsUp to 100500 to 1,000+Draw commands sent from CPU to GPU.
TrianglesVariesMillionsTotal polygon count. Rarely the main bottleneck.

Where Do We Usually Mess Up?

What do these limits actually depend on, and where do we usually mess up? Besides raw hardware power, the screen and canvas size play a huge role. Higher resolutions mean the GPU has to render way more pixels, especially on Retina displays with a high device pixel ratio.

If you have a lot of objects with transparency or heavy materials, a high resolution will kill your performance through overdraw. But the number of draw calls itself is not affected by screen size. Draw calls are purely a CPU load.

The real pain points and dependency screw-ups usually happen in the code architecture. Here are a few classic traps:

  • Recreating geometries or materials inside the render loop instead of reusing them.
  • Messing up dependency arrays in React Three Fiber. This causes components to re-render constantly, spawning new WebGL objects and skyrocketing draw calls without you even noticing.
  • Leaving frustum culling disabled.
  • Importing a single Blender file where the artist forgot to join meshes, leaving you with 2,000 separate nodes for a simple car model.

The CPU Problem

If you have 500 objects in a scene, the CPU has to bother the graphics card 500 times per frame with separate commands. Every single command takes up time on the main thread.

We quickly eat up our 16.6 ms budget. The main bottleneck becomes the CPU, which simply cannot hand out orders fast enough. We can solve this problem using two main approaches: instancing and batching.

Instancing

Instancing is basically a cloning technique. It lets you render a massive number of identical objects with just one draw call.

Instead of creating 10,000 separate trees, we load the tree model into the GPU memory once. Then, we pass an array of data with positions, rotations, and scales for each of the 10,000 clones. The CPU makes one draw call, and the graphics card places all the objects itself. In Three.js, you do this using `InstancedMesh`.

This is perfect for particles like rain or snow, vegetation like grass and identical trees, or a crowd of identical NPCs.

				
					const mesh = new THREE.InstancedMesh(boxGeo, material, count)

for (let i = 0; i < count; i++) {
  dummy.position.set(x[i], 0, z[i])
  dummy.updateMatrix()
  mesh.setMatrixAt(i, dummy.matrix)
}

scene.add(mesh)
				
			

Instancing Pros and Cons

Pros:

  • Massive draw call savings where 10,000 objects equal one call.
  • Very low RAM usage.
  • Easy to update positions in real time.

Cons:

  • Only works for objects with the exact same geometry and one material.
  • Hard to manage individual clones, like deleting one specific object from the middle of the array.

Batching

Batching is different. If instancing is cloning, batching is combining a bunch of different objects into one massive mesh before sending them to the renderer.

It works like a bulk data transfer. We pack different geometries into one array. The CPU tells the graphics card to draw all of it at once, and then goes to do other things. In Three.js, people often use tools like BufferGeometryUtils.mergeGeometries or the newer BatchedMesh for this.

In a real scenario, we compared a Three.js scene with and without batching. The scene had 326 3D objects, lights, an environment, and post-processing.

  • Without batching: Around 500 draw calls. CPU load was 6 to 8 ms.
  • With batching: Dropped down to around 30 draw calls. CPU load went down to 2 to 3 ms.
				
					const batched = new THREE.BatchedMesh(
  maxCount,
  maxVertices,
  maxIndices,
  material
)

const boxId = batched.addGeometry(boxGeo)
const sphereId = batched.addGeometry(sphereGeo)

batched.addInstance(boxId)
batched.addInstance(sphereId)
				
			

Example 1: Unoptimized Scene

This is a standard scene with hundreds of separate objects. Open the performance monitor and check the baseline metrics before any optimization is applied.

  • Where to look: Check the draw calls (they will be high) and note the baseline CPU ms.

Example 2: Unoptimized Scene with CPU Throttling

In this example, we apply CPU throttling to simulate a low-end device or a heavy background load. The processor simply cannot send all those individual draw calls to the GPU fast enough.

  • Where to look: Watch the severe drop in FPS and the massive spike in CPU ms. The high number of draw calls is now bottlenecking the system.

Example 3: Batched Scene with CPU Throttling

This scene runs under the exact same CPU throttling, but all static geometries are merged into a single batch. The CPU only needs to send one command to the graphics card.

  • Where to look: Notice how the draw calls drop to almost zero. Because of this, the CPU ms stays low, and the FPS remains smooth and stable despite the artificial load.

Batching Pros and Cons

Batching lets you combine different geometries and radically lowers CPU load. But it has downsides.

Pros:

Combines different geometries. Radically lowers CPU load.

Cons:

Code gets harder to maintain. Requires texture atlases for multiple materials. Breaks frustum culling. Hard to animate or raycast individual parts.

Borrowing from Classic Gamedev

In classic gamedev with engines like Unity or Unreal, fighting draw calls is a daily routine. Some of their best practices work great on the web.

  • Texture atlasing: To merge 50 different objects into one batch, they need to share a material. Gamedev solves this by baking 50 small textures into one large image called an atlas. The objects just read the parts they need using UV coordinates.
  • LOD (Level of Detail): You swap high-poly models for low-poly ones when they are far from the camera. Three.js has a built-in LOD class for this.
  • Static batching: If objects do not move, like houses or rocks, they get merged into a single mesh while the scene is loading.

Do You Always Need This?

You do not always need to do this. Optimizing just for the sake of optimizing is a bad idea.

You can skip batching and instancing if you are making a simple landing page with one nice 3D model of a shoe. If your scene has 20 to 50 objects and the performance monitor shows a stable 60 FPS and 2 ms CPU time, leave it alone.

The same goes for rapid prototyping. Build the core mechanics first and optimize later. Also, if objects are constantly being created and destroyed with unique geometries, dynamic batching on the fly in JavaScript might eat up more CPU resources than it saves.

When Optimization is Mandatory

But you should definitely start thinking about optimization when your scenes get complex. In web development, optimization is not just about making things work on weak phones.

It is about eliminating frame drops and stutters to keep the experience smooth on all devices, even good ones. If you are building a full browser game, a complex interactive city, or a room configurator with hundreds of items, you need to manage your draw calls.

If you plan to layer a heavy React interface over the canvas or run a physics engine like Cannon.js or Rapier, they will need that CPU time too.

Always profile before you optimize. If you are bottlenecked by the CPU and see over 300 draw calls, it is time to bring out instancing and batching. If the GPU is lagging because of heavy post-processing, batching will not help you, and you will need to lower the resolution or simplify your shaders instead.

Back
minimap_23857_09-24-2026_03-26-09.jpeg
Latest Posts
FREE
Join the Loop

Subscribe for exclusive access to my tailored knowledge base, inspiring content, the latest in tech, and practical tutorials that empower your growth.

By submitting, you agree to receive email updates. Unsubscribe anytime.