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
- 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. - Delta time is the difference between “works” and “works at any frame rate.” Movement must be
speed * dt(per-second units), notspeed * 1per frame — otherwise your game runs 2x faster on a 120Hz screen and janky on a slow one. - 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).
- rAF is not
setInterval: rAF pauses in background tabs (good), is aligned to the display (smooth), and gives you the timestamp —setIntervaldrifts, 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:
dtis in seconds — divide by 1000, and write movement asx += speed * dt(speed in px/s).- 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). - rAF passes the timestamp — you don’t need
Date.now()in the loop.
Fixed vs variable timestep (when each)
| Variable timestep | Fixed timestep | |
|---|---|---|
| How | One update per frame, dt varies | Logic in fixed dt chunks (e.g. 1/60s), render catches up |
| Complexity | Simple | Accumulator logic, interpolation |
| Physics/balance | Drifts at odd frame rates | Stable, deterministic |
| Casual game | ✅ Default choice | Overkill |
| Simulation-heavy | Risky | ✅ 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:
- Draw calls (texture atlases, culling, batching) — the renderer’s biggest cost.
- Per-frame allocations — creating objects/strings every frame → GC pauses.
- Logic — tween counts, physics, input processing.
- 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
speedper frame, not per second — the classic frame-rate-dependence bug; alwaysspeed * dt.- No dt clamp — background-tab resume teleports the game; clamp to ~100ms.
setInterval(16.7)instead of rAF — drifts, runs in background, no vsync; rAF is the browser-correct choice.- Multiple loops — two rAF loops (or an rAF + a setInterval for UI) fight each other; one loop, one heartbeat.
- 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).