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.