Clamp Delta Time in Matter.js for Tab Suspension

When a user switches away from a browser tab, requestAnimationFrame throttles or pauses execution. Upon returning, the accumulated elapsed time produces a massive delta time spike, causing Matter.js bodies to tunnel through walls or violently eject across the canvas. This article explains how to prevent physics explosions by capping the maximum delta time supplied to the simulation, ensuring stability regardless of how long a tab remains suspended.

Why Browser Tab Suspensions Break Physics

Modern browsers aggressively optimize background tabs by lowering the execution frequency of timers and completely freezing requestAnimationFrame. If your game loop calculates the time difference between the current frame and the previous frame directly:

const delta = currentTime - lastTime;

A tab inactive for 10 seconds will pass a delta of 10000 milliseconds to the next update call. Matter.js attempts to resolve positional displacement across this entire duration in a single step, resulting in extreme velocity vectors, missed collisions (tunneling), and overall engine destabilization.

Implementing a Custom Loop with Delta Clamping

The most robust way to protect Matter.js from tab suspension spikes is to bypass the default Matter.Runner in favor of a custom animation loop that bounds the delta parameter using Math.min.

const engine = Matter.Engine.create();
const maxDelta = 1000 / 30; // Maximum allowed step: ~33.33ms (equivalent to 30 FPS)
let lastTime = performance.now();

function gameLoop(currentTime) {
    const rawDelta = currentTime - lastTime;
    lastTime = currentTime;

    // Clamp the delta to prevent physics explosion after tab switch
    const clampedDelta = Math.min(rawDelta, maxDelta);

    Matter.Engine.update(engine, clampedDelta);

    requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

By clamping rawDelta to maxDelta, the physics engine processes no more than the equivalent of a 30 FPS frame duration when recovering from a background freeze, keeping collisions consistent and predictable.

Using a Fixed Timestep

Alternatively, you can decouple physics from variable frame rendering altogether. A fixed timestep ensures that the engine updates with an identical delta on every tick, regardless of browser performance or background throttling.

const engine = Matter.Engine.create();
const fixedDelta = 1000 / 60; // Exact 60 FPS timestep (16.66ms)

function fixedLoop() {
    Matter.Engine.update(engine, fixedDelta);
    requestAnimationFrame(fixedLoop);
}

requestAnimationFrame(fixedLoop);

With this approach, when a tab resumes after suspension, Matter.js simply takes a single standard 16.66ms step instead of attempting to simulate the elapsed real-world time.

Handling the Page Visibility API

To pair delta clamping with clean state restoration, use the Page Visibility API to reset your timing counter immediately upon focus recovery:

let lastTime = performance.now();

document.addEventListener('visibilitychange', () => {
    if (!document.hidden) {
        // Reset timestamp so the next frame computes delta starting from now
        lastTime = performance.now();
    }
});

Resetting lastTime on visibility changes ensures the initial active frame computes a near-zero delta, completely bypassing the need for heavy clamping on the transition frame.