Optimizing Hundreds of Circles in Matter.js

Simulating hundreds of physics bodies simultaneously can cause severe frame rate drops in a web browser. When dealing with hundreds of identical circles in Matter.js, performance bottlenecks typically stem from the default debug renderer, excessive collision solver iterations, and unoptimized broadphase collision detection. By decoupling rendering from the physics loop, enabling body sleeping, adjusting engine iteration counts, and leveraging collision filtering, you can achieve smooth, 60-frames-per-second performance even with massive body counts.

1. Replace Matter.Render with a Custom Renderer

The built-in Matter.Render module is intended strictly for development and debugging. It creates new paths and clears the entire canvas context inefficiently on every frame.

For high-count simulations, bypass Matter.Render entirely and draw the circles manually using a plain HTML5 2D Canvas loop or a WebGL framework such as PixiJS. Because the circles are identical, you can batch draw calls:

// Example custom 2D canvas draw loop
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#3498db';
  ctx.beginPath();
  
  for (let i = 0; i < circles.length; i++) {
    const { x, y } = circles[i].position;
    ctx.moveTo(x + radius, y);
    ctx.arc(x, y, radius, 0, Math.PI * 2);
  }
  
  ctx.fill();
  requestAnimationFrame(render);
}

Drawing all circles in a single continuous path using moveTo and a single fill() call dramatically reduces draw call overhead.

2. Lower Engine Iteration Counts

Matter.js defaults to 6 position iterations and 4 velocity iterations per update step to resolve constraints and collisions. Circles have simple, uniform bounding volumes that do not require high precision to prevent penetration.

Lowering these values substantially reduces CPU calculation time per frame:

engine.positionIterations = 2;
engine.velocityIterations = 2;

Reducing both iterations to 2 or 3 usually yields indistinguishable visual differences for circular bodies while halving the collision resolution cost.

3. Enable Body Sleeping

When circles settle on the floor or rest against each other, the physics engine continues resolving potential micro-movements unless explicitly told not to.

Enable sleeping on the engine instance:

engine.enableSleeping = true;

When enabled, bodies that fall below a motion threshold enter a sleep state and are skipped during the collision detection and solver phases until an external force acts on them.

4. Ensure Native Circle Primitives

When creating bodies, verify you are using Bodies.circle without overriding vertex counts:

const circle = Matter.Bodies.circle(x, y, radius, {
  restitution: 0.8,
  friction: 0.05
});

Matter.js provides specialized, highly optimized circle-versus-circle collision detection algorithms based purely on distance and radius math (\(r_1 + r_2\)). Avoid approximating circles using regular polygons (Bodies.polygon), which forces the engine to run the significantly more expensive Separating Axis Theorem (SAT) against multiple vertices.

5. Use Collision Filtering

If certain circles do not need to interact with each other (for example, decorative particles or layered items), use collision bitmasks via collisionFilter. This prevents the engine from executing narrowphase collision checks between non-colliding entities:

const circle = Matter.Bodies.circle(x, y, radius, {
  collisionFilter: {
    category: 0x0002,
    mask: 0x0001 // Only collides with boundaries (category 0x0001), not other circles
  }
});

6. Offload Physics to a Web Worker

If JavaScript execution on the main thread is competing with user interface events or complex DOM updates, move the Matter.js instance to a Web Worker.

Run Engine.update inside the worker using setInterval or a custom timer, then transfer an array of positions (e.g., a shared Float32Array) back to the main thread for rendering. This prevents heavy physics calculations from causing input lag or UI stutter.