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:

  1. Process input — read keyboard, mouse, touch, gamepad, or network packets and translate them into intent (move left, jump, place bet).
  2. Update simulation — advance game state: positions, velocities, AI, timers, win/loss checks. This is logic, not pixels.
  3. 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 requestAnimationFrame in the browser.
  • Use delta time so movement is frame-rate independent; clamp extreme dt after 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