rAF vs setInterval for Animation Loops

setInterval(loop, 16) looks like 60fps, but it's not. Here's why requestAnimationFrame is the only correct tool for interactive simulations.

Every simulation on this site uses a requestAnimationFrame (rAF) loop. The reasons go beyond "it's the modern way" — there are concrete, measurable differences that affect correctness, performance, and user experience.

What's Actually Wrong With setInterval?

setInterval(fn, 16) fires every 16ms whether or not the browser is ready to render. This creates three problems:

What requestAnimationFrame Provides

rAF callbacks receive a DOMHighResTimeStamp parameter with sub-millisecond precision (typically). The callback fires at the display refresh rate — the browser schedules it just before the next paint, in sync with the compositor.

The Correct Pattern

let prevTime = performance.now();

function loop(time) {
  const dt = Math.min((time - prevTime) / 1000, 0.05); // seconds, clamped to 50ms
  prevTime = time;

  update(dt);   // physics step — multiply accelerations by dt, velocities by dt
  render();     // draw

  requestAnimationFrame(loop);
}

requestAnimationFrame(loop); // start

The Math.min(..., 0.05) clamp is important: if the tab was hidden and then returned, dt could be several seconds, causing objects to teleport. Clamping limits max physics step to 50ms so a brief lag doesn't explode the simulation.

Fixed Timestep (for deterministic physics)

For stable physics (collision detection, spring dynamics), a variable dt can cause instability at low FPS. Use a fixed timestep accumulator:

const FIXED_DT = 1 / 60; // 60Hz physics
let accumulator = 0;

function loop(time) {
  const frameTime = Math.min((time - prevTime) / 1000, 0.1);
  prevTime = time;
  accumulator += frameTime;

  while (accumulator >= FIXED_DT) {
    physicsStep(FIXED_DT);
    accumulator -= FIXED_DT;
  }

  render();
  requestAnimationFrame(loop);
}

This runs as many physics steps as needed to "catch up" to real time, while each step is always exactly 1/60s. Render may happen between steps — use the leftover accumulator / FIXED_DT fraction to interpolate render state for smooth visuals even at 120Hz.

✅ requestAnimationFrame

  • Vsync-aligned, no tearing
  • Auto-pauses in hidden tabs
  • Accurate high-res timestamp
  • Browser-optimised scheduling
  • Uses less CPU when hidden

❌ setInterval

  • Not vsync-aligned
  • Runs in hidden tabs (battery drain)
  • Timer drift under load
  • No built-in delta time
  • Can cause "spiral of death" catch-up

Rule: Never use setInterval for animation. Use requestAnimationFrame with a delta-time accumulator for variable-rate rendering, and add a fixed-timestep inner loop for physics stability.