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:
- Main Thread: Handles DOM events, collects user input (mouse, keyboard, touch), and renders graphics using the HTML5 Canvas, WebGL, or a library like PixiJS or Three.js.
- Worker Thread: Initializes the
Matter.Engine, updates the physics world at fixed intervals, and transmits the resulting body transforms (coordinates and rotation angles) back to the main thread.
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:
- Allocate a
Float32Arrayrepresenting[id, x, y, angle]for each entity. - Transfer the buffer's ownership using
self.postMessage(buffer, [buffer.buffer]). - Use a shared indexing mechanism so the main thread can map flat array offsets directly to the corresponding visual entities without JSON serialization delays.