How requestAnimationFrame Affects Matter.js Tick Rate

This article explores how the browser's requestAnimationFrame API directly governs the execution and tick rate of physics simulations in Matter.js. You will learn how browser rendering loops synchronize with the Matter.js engine, why varying monitor refresh rates and dropped frames alter simulation behavior, and how to stabilize physics calculations across different hardware environments.

The Connection Between requestAnimationFrame and Matter.js

By default, Matter.js delegates its timing logic to Matter.Runner, which relies on window.requestAnimationFrame (rAF) to synchronize simulation updates with the browser’s render cycle. When you start an engine with Matter.Runner.run(runner, engine), the runner invokes requestAnimationFrame on a continuous loop. On each frame, it calculates the elapsed time (delta) since the previous call and passes this value to Engine.update(engine, delta).

Because requestAnimationFrame is tethered to the display subsystem of the user's device, the tick rate of Matter.js is fundamentally tied to the client's screen refresh rate rather than a guaranteed internal timer.

Display Refresh Rates and Tick Inconsistency

The frequency at which requestAnimationFrame executes varies significantly across devices:

If Matter.Runner is set to run with a dynamic delta based strictly on the timestamp provided by requestAnimationFrame, a 120Hz monitor will run twice as many update cycles per second as a 60Hz monitor. While a dynamically calculated delta attempts to compensate by stepping the physics by smaller increments on faster displays, numerical integration in physics engines (specifically Verlet or Euler integration) is not completely scale-invariant. Consequently, springs, constraints, and friction can behave differently at 120Hz compared to 60Hz.

Frame Drops, Background Tabs, and the "Spiral of Death"

Using requestAnimationFrame means that the tick rate drops whenever the main thread is blocked or the browser tab loses focus.

  1. Background Throttling: Browsers pause or throttle requestAnimationFrame calls to 1Hz or lower when a user switches tabs to conserve battery and CPU resources. When the tab regains focus, the delta time between the previous frame and the current frame can be hundreds or thousands of milliseconds. If passed directly to Engine.update(), this massive delta can launch bodies through walls (tunneling) or tear constraints apart.
  2. Delta Clamping: Matter.Runner mitigates large jumps by enforcing an upper bound on the delta (controlled by runner.isFixed and default clamping mechanics), discarding excessively large time jumps. However, this causes the physics simulation to appear in slow motion during severe performance dips rather than maintaining real-world time synchronization.

Achieving Consistent Physics with a Fixed Timestep

To eliminate hardware-dependent physics discrepancies caused by requestAnimationFrame, you can decouple the physics step from the visual frame rate using a fixed timestep with an accumulator.

Instead of allowing requestAnimationFrame to pass arbitrary delta values directly into Engine.update(), use requestAnimationFrame solely for rendering and tracking accumulated real time:

const engine = Matter.Engine.create();
const fixedDelta = 1000 / 60; // Exact 60Hz physics step (16.66ms)
let lastTime = performance.now();
let accumulator = 0;

function loop(currentTime) {
    const frameTime = Math.min(currentTime - lastTime, 100); // Clamp maximum frame time
    lastTime = currentTime;
    accumulator += frameTime;

    while (accumulator >= fixedDelta) {
        Matter.Engine.update(engine, fixedDelta);
        accumulator -= fixedDelta;
    }

    // Trigger visual rendering here
    requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Using this pattern, requestAnimationFrame still drives the drawing loop, but the physics engine consistently computes deterministic updates regardless of whether the user is on a 60Hz office monitor or a 240Hz gaming display.