Guide
Game loop and frame timing explained
Every interactive game — from a browser canvas to a AAA console title — runs on a
game loop: a cycle that reads input, advances simulation, and draws
the next frame. How you measure elapsed time between frames and how
you apply it to physics determines whether movement feels crisp or mushy, whether
collisions are reliable, and whether your title holds 60 frames per second on a
mid-range phone. This guide explains the anatomy of a loop, delta time,
fixed vs variable timestep, rendering with
requestAnimationFrame, and the practical tricks engines use to hide
stutter when the GPU cannot keep up.
The three phases of a game loop
At its core, a loop repeats three jobs until the player quits:
- Process input — read keyboard, mouse, touch, gamepad, or network packets and translate them into intent (move left, jump, place bet).
- Update simulation — advance game state: positions, velocities, AI, timers, win/loss checks. This is logic, not pixels.
- Render — draw the current state to screen (Canvas 2D, WebGL, DOM, or native GPU APIs).
Novice loops often merge update and render into one block. That works for tiny prototypes, but separating them pays off quickly. Simulation should be deterministic enough to replay — critical for networked games, replays, and provably-fair mechanics where the outcome must be verifiable from seeds and inputs. Rendering can skip or interpolate when frames drop without changing who won the round.
A minimal browser loop looks like this conceptually:
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000; // seconds since last frame
last = now;
readInput();
updateSimulation(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
requestAnimationFrame (rAF) schedules work before the next compositor
paint — typically aligned to the display refresh (60 Hz on most monitors, 120 Hz
on high-refresh phones). Unlike setInterval, rAF pauses in background
tabs, saving battery. For games that must simulate while hidden (idle clickers),
you may need a hybrid: rAF when visible, throttled timers when not.
Delta time: making speed independent of frame rate
If you move a character 5 pixels per frame, a 30 FPS machine travels
half as fast as a 120 FPS machine. Players notice immediately. The fix is
delta time (dt): multiply rates by elapsed seconds.
player.x += player.speed * dt; // speed in pixels per second
Express velocities, accelerations, and cooldowns in per-second units.
A jump impulse of 400 px/s upward with gravity -980 px/s²
behaves identically at 30 or 144 FPS when integrated with dt.
Clamping delta time
When a tab loses focus or the OS stalls, dt can spike to several
seconds. Uncapped integration launches characters through walls. Common practice:
clamp dt to a maximum (often 1/30 or 1/15
second) so one bad hitch does not destroy simulation. Document the clamp — speedrun
communities care about whether pauses affect physics.
Frame budget
At 60 FPS, each frame has roughly 16.7 milliseconds of wall-clock time for JavaScript, layout, and GPU work combined. At 120 FPS, the budget halves to ~8.3 ms. Profiling with browser DevTools Performance panel shows whether update or render dominates. Our Core Web Vitals guide covers how interaction latency (INP) relates to main-thread work — the same constraints apply inside games.
Fixed timestep vs variable timestep
Variable timestep
Pass the actual dt each frame into physics. Simple and smooth when
frame times are stable. Problems appear with stiff constraints, stacked collisions,
and integrator error: large dt steps make tunneling (objects passing
through each other) more likely. Verlet and semi-implicit Euler help but do not
eliminate the issue.
Fixed timestep
Advance simulation in constant slices — classically 1/60 s or 1/120 s — regardless of display refresh. If rendering is faster than simulation, you may run zero or one physics steps per frame; if rendering is slower, you run multiple physics steps to catch up (the spiral of death if unchecked).
const FIXED_DT = 1 / 60;
let accumulator = 0;
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.25);
last = now;
accumulator += dt;
while (accumulator >= FIXED_DT) {
updateSimulation(FIXED_DT);
accumulator -= FIXED_DT;
}
const alpha = accumulator / FIXED_DT; // 0..1 between steps
render(lerp(previousState, currentState, alpha));
requestAnimationFrame(frame);
}
The alpha blend between previous and current state is
interpolation — rendering at display rate while physics ticks at a
fixed rate. Fighting games and platformers often insist on fixed steps so combo
timing and hitboxes match across hardware.
When to choose which
- Variable — casual UI games, card games, turn-based logic, anything without continuous collision.
- Fixed — physics-heavy platformers, racing, networked lockstep, reproducible simulations.
- Hybrid — fixed physics, variable animation blending; common in commercial engines (Unity, Godot, Bevy).
Update rate vs render rate
Display refresh and simulation rate need not match. A 30 Hz simulation interpolated to 120 Hz can look smooth while cutting CPU cost. Conversely, running simulation at 240 Hz on a 60 Hz panel wastes work unless sub-frame accuracy matters (competitive shooters, physics puzzles).
On mobile, thermal throttling drops CPU clocks mid-session. Games that assume a fixed 16.7 ms budget stutter hard. Adaptive quality — lowering particle counts, shadow resolution, or physics substeps when frame time exceeds a threshold — keeps experience playable. Track a rolling average of frame times, not just the last frame.
Background tabs and low-power mode may cap rAF to 30 FPS or pause entirely. If your game includes real-time multiplayer, pair local loops with authoritative server ticks or snapshot interpolation; our WebSockets and SSE guide covers transport choices for pushing state to clients.
Input sampling and one-frame delays
Where in the loop you poll input matters. Sampling at the start of update means rendering reflects slightly stale input — usually one frame (~16 ms) of lag. Fast-twitch genres sometimes read input after physics and before render, or use raw device timestamps, to minimize perceived latency.
For pointer lock FPS controls, accumulate mouse deltas per frame rather than applying
absolute cursor position. Touch games should distinguish touchstart
from continuous touchmove and guard against ghost clicks when scrolling
is also enabled on the page.
Determinism, seeds, and fair outcomes
Wallet-native and provably-fair games add another constraint: given the same seed and inputs, the outcome must recompute identically on any machine. That means:
- Avoid
Math.random()for outcomes — use a seeded PRNG (xorshift, mulberry32, HMAC-derived streams). - Prefer integer or fixed-point math for verdict logic; floating-point rounding differs across CPUs unless you standardize.
- Separate outcome resolution (one-shot, seed-driven) from presentation (animated dice spin over many frames).
Animation can run at variable dt while the result was decided in a
single fixed step when the player committed. Garden Dice follows this pattern —
see our provably fair Solana games guide
for commit-reveal verification that does not depend on frame rate.
Common pitfalls
- Spiral of death — uncapped catch-up physics steps after a hitch; cap max steps per frame (e.g. 5) and slow simulation time instead.
- Mixing units — some systems use per-frame speeds, others per-second; standardize early.
- Garbage collection spikes — allocating new objects every frame (particles, strings) causes periodic stutter; pool and reuse.
- Layout thrashing in DOM games — reading layout then mutating styles in a loop forces synchronous reflow; batch DOM writes or move to Canvas/WebGL.
- Assuming vsync — rAF aligns to refresh when the tab is focused, but not when throttled; test on real devices.
- Ignoring
devicePixelRatio— canvas bitmap size must match CSS size times DPR or visuals blur on Retina displays.
Key takeaways
- A game loop is input → update → render, scheduled with
requestAnimationFramein the browser. - Use delta time so movement is frame-rate independent; clamp extreme
dtafter hitches. - Fixed timestep physics with render interpolation gives stable collisions; variable timestep suits lightweight logic.
- Budget ~16.7 ms per frame at 60 FPS — profile update vs render separately.
- Separate deterministic outcome logic from cosmetic animation, especially for verifiable or on-chain games.
Related reading
- Core Web Vitals explained — INP, LCP, and main-thread responsiveness budgets
- WebSockets and server-sent events — real-time state sync for multiplayer
- Provably fair Solana games — separating verifiable outcomes from animation
- CPU process scheduling — how the OS shares cores when your loop competes with other work