Integrate Matter.js with requestAnimationFrame
This article explains how to manually drive the Matter.js physics
engine using the browser's native requestAnimationFrame API
instead of the default Matter.Runner. You will learn how to
bypass the automated runner, decouple your physics updates from the
display refresh rate, and implement both a basic game loop and an
advanced fixed-timestep loop with an accumulator to ensure
deterministic, smooth physics behavior across various screen refresh
rates.
Why Use requestAnimationFrame Instead of Matter.Runner?
By default, Matter.js provides Matter.Runner to automate
engine updates. However, managing your own game loop with
requestAnimationFrame is necessary when:
- You are rendering with external libraries like PixiJS, Three.js, or custom Canvas2D contexts.
- You need precise control over the execution order (e.g., input handling -> physics update -> render).
- You want to maintain consistent physics calculations across monitors with different refresh rates (60Hz, 120Hz, 144Hz+).
Basic Implementation
To integrate Matter.js directly with
requestAnimationFrame, call
Engine.update(engine, delta) inside your animation callback
function instead of launching Matter.Runner.run().
import Matter from 'matter-js';
const { Engine, World, Bodies } = Matter;
// 1. Create the engine
const engine = Engine.create();
// Add bodies to the world
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
World.add(engine.world, [box, ground]);
// 2. Setup the loop variables
let lastTime = performance.now();
let animationFrameId;
// 3. Define the update loop
function loop(currentTime) {
// Calculate time elapsed since last frame (in milliseconds)
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Update Matter.js engine
Matter.Engine.update(engine, deltaTime);
// Custom render logic goes here (e.g., drawing bodies to canvas)
// Request next frame
animationFrameId = requestAnimationFrame(loop);
}
// 4. Start the loop
animationFrameId = requestAnimationFrame(loop);To stop the loop at any time:
cancelAnimationFrame(animationFrameId);Advanced Implementation: Fixed Timestep with an Accumulator
Passing a variable deltaTime directly to
Engine.update can lead to unstable physics, clipping
through geometry, or inconsistent behavior if the frame rate drops.
A production-grade approach uses a fixed timestep with an accumulator. This decouples the physics calculation frequency from the monitor's display refresh rate.
import Matter from 'matter-js';
const engine = Matter.Engine.create();
let lastTime = performance.now();
let accumulator = 0;
const fixedDelta = 1000 / 60; // Fixed 60 updates per second (~16.67ms)
let animationFrameId;
function loop(currentTime) {
// Calculate time passed since last tick
let frameTime = currentTime - lastTime;
lastTime = currentTime;
// Prevent spiral of death on lag spikes or tab switching
if (frameTime > 250) {
frameTime = 250;
}
accumulator += frameTime;
// Consume accumulated time in fixed slices
while (accumulator >= fixedDelta) {
Matter.Engine.update(engine, fixedDelta);
accumulator -= fixedDelta;
}
// Render your scene after all physics steps for this frame are complete
render();
animationFrameId = requestAnimationFrame(loop);
}
function render() {
// Clear canvas and draw updated body positions here
}
animationFrameId = requestAnimationFrame(loop);Summary Checklist for Integration
- Omit
Matter.Runner: Do not invokeMatter.Runner.run(), as running both will cause the engine to update twice per frame. - Handle Large Deltas: Always clamp
frameTimewhen tabs lose focus to prevent the accumulator from running hundreds of updates simultaneously upon refocusing. - Separate Physics and Rendering: Run all physics steps before drawing to the screen to ensure visuals always reflect the latest physics state.