The Game Loop in HTML5: requestAnimationFrame, Delta Time, and 60fps

The Game Loop in HTML5: requestAnimationFrame, Delta Time, and 60fps

Honesty note: this is the loop theory behind my Phaser game — Phaser runs its own loop internally, but understanding the raw mechanism is what lets you debug frame drops and write engine-free prototypes (my 60fps guide covers the practical side).

TL;DR

  1. The game loop is the heartbeat: update game state, then render — every frame, forever. In HTML5 the scheduler is requestAnimationFrame (rAF): the browser calls your callback once per display refresh (~16.7ms at 60Hz), and only when the tab is visible.
  2. Delta time is the difference between “works” and “works at any frame rate.” Movement must be speed * dt (per-second units), not speed * 1 per frame — otherwise your game runs 2x faster on a 120Hz screen and janky on a slow one.
  3. Two loop architectures: fixed timestep (logic runs in fixed chunks, render as often as possible — stable physics/simulation) and variable timestep (one update per frame with dt — simple, but physics/balance can drift). Casual web games mostly want variable dt; simulation-heavy games want fixed (physics decision).
  4. rAF is not setInterval: rAF pauses in background tabs (good), is aligned to the display (smooth), and gives you the timestamp — setInterval drifts, keeps running in background, and has no vsync alignment.

The minimal loop (engine-free)

let last = performance.now();

function frame(now) {
  const dt = (now - last) / 1000;   // seconds since last frame
  last = now;

  update(dt);   // game logic, frame-rate independent
  render();     // draw this frame

  requestAnimationFrame(frame);     // schedule the next
}

requestAnimationFrame(frame);

Three things to notice:

  1. dt is in seconds — divide by 1000, and write movement as x += speed * dt (speed in px/s).
  2. Clamp dt (e.g., Math.min(dt, 0.1)) — after a background tab pause, the first dt can be seconds long, teleporting your game; clamp prevents the jump (the 60fps guide).
  3. rAF passes the timestamp — you don’t need Date.now() in the loop.

Fixed vs variable timestep (when each)

Variable timestepFixed timestep
HowOne update per frame, dt variesLogic in fixed dt chunks (e.g. 1/60s), render catches up
ComplexitySimpleAccumulator logic, interpolation
Physics/balanceDrifts at odd frame ratesStable, deterministic
Casual game✅ Default choiceOverkill
Simulation-heavyRisky✅ Right call

The honest rule: a casual merge/tile game with tweens and no physics runs variable dt happily (why no physics); a platformer with physics or a networked game wants fixed timestep. Don’t build the accumulator until a game problem demands it.

How Phaser spends the 16.7ms (for context)

Phaser’s internal loop (which uses rAF) runs: update logic → pre-render → render. Where time goes in practice:

  1. Draw calls (texture atlases, culling, batching) — the renderer’s biggest cost.
  2. Per-frame allocations — creating objects/strings every frame → GC pauses.
  3. Logic — tween counts, physics, input processing.
  4. Assets — oversized textures cost GPU memory and fill rate.

That’s the exact priority order of the 60fps optimization guide — the loop’s budget is spent in that order, so you optimize in that order.

Pitfalls

  1. speed per frame, not per second — the classic frame-rate-dependence bug; always speed * dt.
  2. No dt clamp — background-tab resume teleports the game; clamp to ~100ms.
  3. setInterval(16.7) instead of rAF — drifts, runs in background, no vsync; rAF is the browser-correct choice.
  4. Multiple loops — two rAF loops (or an rAF + a setInterval for UI) fight each other; one loop, one heartbeat.
  5. Fixed timestep when not needed — the accumulator adds complexity and interpolation bugs to a game that never needed determinism.

Bottom line

The HTML5 game loop is rAF + delta time: update with speed * dt, clamp dt, render, repeat — and choose variable timestep for casual games, fixed only when physics/simulation demands determinism. It’s the same heartbeat Phaser runs internally, and understanding it is what turns “the game stutters” into “the draw calls are the problem, fix the atlas first” (the full budget).