How to Run Matter.js in a Web Worker

Running Matter.js inside a Web Worker allows you to offload heavy physics simulations from the browser's main execution thread, preventing UI lag and frame rate drops. By isolating the physics step to background threads, your application can maintain a steady 60 frames per second on the rendering side while the worker calculates complex rigid-body dynamics, collisions, and constraints independently.

Architecture Overview

To decouple Matter.js from the main thread, you must separate physics updates from rendering and input handling:

Because Web Workers lack direct access to the DOM, you cannot use the built-in Matter.Render module inside the worker. You must compute transforms purely through Matter.Engine and handle rendering manually on the main thread.

Step 1: Configuring the Worker

Create a dedicated worker file (for example, physics.worker.js). In this file, import Matter.js, set up the engine, create your bodies, and run a simulation loop.

import Matter from 'matter-js';

const { Engine, Bodies, Composite } = Matter;

const engine = Engine.create();
const world = engine.world;

// Create physics bodies
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
Composite.add(world, [box, ground]);

// Simulation loop
const FIXED_TIMESTEP = 1000 / 60;

function step() {
  Engine.update(engine, FIXED_TIMESTEP);

  // Extract transform data to send to the main thread
  const payload = [
    { id: box.id, x: box.position.x, y: box.position.y, angle: box.angle }
  ];

  self.postMessage({ type: 'TICK', bodies: payload });
}

setInterval(step, FIXED_TIMESTEP);

// Handle messages from the main thread
self.onmessage = (event) => {
  const { type, data } = event.data;
  if (type === 'APPLY_FORCE') {
    Matter.Body.applyForce(box, box.position, data.force);
  }
};

Step 2: Setting Up the Main Thread

The main thread creates the worker instance, listens for updated body positions, and draws them to the screen using requestAnimationFrame.

const canvas = document.getElementById('renderCanvas');
const ctx = canvas.getContext('2d');

const worker = new Worker(new URL('./physics.worker.js', import.meta.url), {
  type: 'module'
});

let sceneBodies = [];

// Receive updated positions from the physics engine
worker.onmessage = (event) => {
  if (event.data.type === 'TICK') {
    sceneBodies = event.data.bodies;
  }
};

// Rendering loop
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  sceneBodies.forEach((body) => {
    ctx.save();
    ctx.translate(body.x, body.y);
    ctx.rotate(body.angle);
    ctx.fillRect(-40, -40, 80, 80);
    ctx.restore();
  });

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

// Send interaction events to the worker
window.addEventListener('click', () => {
  worker.postMessage({
    type: 'APPLY_FORCE',
    data: { force: { x: 0, y: -0.05 } }
  });
});

Minimizing Communication Overhead

Passing large JavaScript objects via postMessage introduces serialization and cloning overhead. For high-performance simulations involving thousands of bodies, use ArrayBuffer or typed arrays (such as Float32Array) along with Transferable Objects.

Instead of passing an array of objects:

  1. Allocate a Float32Array representing [id, x, y, angle] for each entity.
  2. Transfer the buffer's ownership using self.postMessage(buffer, [buffer.buffer]).
  3. Use a shared indexing mechanism so the main thread can map flat array offsets directly to the corresponding visual entities without JSON serialization delays.