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:

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

  1. Avoid Zero Iterations: Setting iterations to 0 causes collisions to fail completely. Maintain a hard floor of at least 2 for positionIterations and 1 for velocityIterations to prevent bodies from passing through each other entirely.
  2. 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.
  3. 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.