Dynamic Solver Iterations in Matter.js by FPS
Matter.js relies on iterative constraint solvers to resolve
collisions and physics constraints accurately. Under heavy simulation
loads or hardware strain, physics processing can overload the main
execution thread, leading to noticeable frame rate drops. This article
explains how to monitor the frame rate in real time and dynamically
scale down the engine's positionIterations and
velocityIterations during performance dips, automatically
restoring them once the frame rate stabilizes to maintain an optimal
balance between visual smoothness and physical accuracy.
Understanding Solver Iterations
Matter.js uses two core properties on the Engine
instance to control physics fidelity:
engine.positionIterations(default: 6): Determines how strictly overlapping bodies are pushed apart.engine.velocityIterations(default: 4): Determines how accurately velocities and impulses are transferred upon impact.
Higher values produce stiffer, more stable physics but require significantly more CPU cycles per step. Reducing these values reduces computational overhead, freeing up thread time for the browser to render frames at the target rate.
Implementing Dynamic Iteration Scaling
To scale iterations adaptively, track the elapsed time between frames, calculate a moving average of the frame rate, and adjust iteration values when thresholds are crossed.
const { Engine, Render, Runner, World, Bodies, Events } = Matter;
const engine = Engine.create();
const runner = Runner.create();
// Configuration limits
const ITERATION_CONFIG = {
targetFps: 60,
minPosition: 2,
maxPosition: 6,
minVelocity: 1,
maxVelocity: 4,
fpsDropThreshold: 45,
fpsRecoveryThreshold: 55
};
// FPS calculation variables
let lastTime = performance.now();
let frameCount = 0;
let fps = 60;
let fpsCheckInterval = 500; // Recalculate every 500ms
let lastFpsUpdate = performance.now();
function updateSolverFidelity(currentFps) {
if (currentFps < ITERATION_CONFIG.fpsDropThreshold) {
// Under heavy load: drop to lower fidelity to recover frame rate
engine.positionIterations = Math.max(
ITERATION_CONFIG.minPosition,
engine.positionIterations - 1
);
engine.velocityIterations = Math.max(
ITERATION_CONFIG.minVelocity,
engine.velocityIterations - 1
);
} else if (currentFps >= ITERATION_CONFIG.fpsRecoveryThreshold) {
// Performance has recovered: step fidelity back up
engine.positionIterations = Math.min(
ITERATION_CONFIG.maxPosition,
engine.positionIterations + 1
);
engine.velocityIterations = Math.min(
ITERATION_CONFIG.maxVelocity,
engine.velocityIterations + 1
);
}
}
// Hook into engine updates to monitor frame rate
Events.on(engine, 'beforeUpdate', () => {
const now = performance.now();
frameCount++;
if (now - lastFpsUpdate >= fpsCheckInterval) {
fps = (frameCount * 1000) / (now - lastFpsUpdate);
frameCount = 0;
lastFpsUpdate = now;
updateSolverFidelity(fps);
}
});Critical Trade-offs and Best Practices
- Avoid Zero Iterations: Setting iterations to
0causes collisions to fail completely. Maintain a hard floor of at least2forpositionIterationsand1forvelocityIterationsto prevent bodies from passing through each other entirely. - Prevent Rapid Oscillation (Hysteresis): Do not use a single threshold for scaling up and down. Using separated boundaries (e.g., drop below 45 FPS, recover above 55 FPS) prevents the engine from constantly toggling iteration counts every second.
- Tunneling Mitigation: Lowering iterations increases the risk of "tunneling" (fast-moving objects passing through boundaries). Ensure static boundaries have adequate thickness to compensate for lower position resolution during dropped-frame intervals.