Game Engine Architecture

HTML5 Canvas 2D Game Loop Architecture: Delta Time, Fixed Timesteps & Interpolation

The game loop is the beating heart of any interactive software application. In the browser environment, where refresh rates fluctuate across 60Hz, 120Hz ProMotion, and 240Hz gaming monitors, architecting a deterministic, stutter-free loop requires mastering the separation of physical simulation time from presentation rendering.

The Fatal Flaws of Variable Delta Time

Most introductory HTML5 Canvas tutorials introduce a naive game loop that captures elapsed time between successive requestAnimationFrame callbacks and multiplies velocities directly:

// Naive Variable Delta Time Loop (Antipattern)
function loop(timestamp) {
  const dt = (timestamp - lastTime) / 1000; // Variable seconds
  lastTime = timestamp;

  player.x += player.vx * dt; // Vulnerable to tunneling!
  player.vy += GRAVITY * dt;

  ctx.clearRect(0, 0, width, height);
  render();
  requestAnimationFrame(loop);
}

While this appears functional under clean lab conditions, it fails catastrophically in production. The root cause lies in the numerical instability of variable-step numerical integration. In games featuring physics simulations, collision detection, or platforming jumps, physical equations assume that time proceeds in infinitesimal, uniform increments.

When a browser tab experiences an intermittent 100ms garbage collection pause, dt suddenly surges from 0.016s to 0.1s. A bullet travelling at 1,000 pixels per second advances 100 pixels in a single frame, completely bypassing a 10-pixel-thick wall collision boundary—a phenomenon known as collision tunneling. Furthermore, jumps calculated with variable steps result in non-deterministic trajectories, making speedruns impossible and introducing irreproducible physics anomalies.

Glenn Fiedler's Canonical Fixed Timestep Accumulator Pattern

The gold standard solution for deterministic game physics is the Fixed Timestep with Accumulator pattern, famously popularized by Glenn Fiedler ("Fix Your Timestep!"). The core architectural insight is that physics simulation must never run at the whim of the monitor refresh rate.

The architecture maintains an internal "time bank" (the accumulator). On every frame render, the elapsed real-world time is credited to the accumulator. The engine then consumes this accumulated time in discrete, immutable chunks (such as exactly 20 milliseconds, representing a deterministic 50Hz physics rate).

If a frame is displayed on a 120Hz display (8.33ms between renders), the accumulator will only trigger a physics update every other frame. If a frame drops to 30 FPS, the loop simply executes two or three fixed physics updates in rapid succession to catch up:

// Canonical Fixed Timestep Loop with Spiral-of-Death Protection
class CanonicalGameLoop {
  private lastTime: number = 0;
  private accumulator: number = 0;
  private readonly fixedStep: number = 1000 / 60; // 16.666ms (60Hz fixed simulation)
  private readonly maxFrameTime: number = 250; // Prevent spiral of death

  public start(): void {
    this.lastTime = performance.now();
    requestAnimationFrame(this.step.bind(this));
  }

  private step(currentTime: number): void {
    let frameTime = currentTime - this.lastTime;
    this.lastTime = currentTime;

    // Spiral-of-Death Guard: clamp massive frame times
    if (frameTime > this.maxFrameTime) {
      frameTime = this.maxFrameTime;
    }

    this.accumulator += frameTime;

    // Consume time in exact deterministic chunks
    while (this.accumulator >= this.fixedStep) {
      this.savePreviousStates(); // For interpolation
      this.updatePhysics(this.fixedStep);
      this.accumulator -= this.fixedStep;
    }

    // Calculate alpha for butter-smooth visual interpolation
    const alpha = this.accumulator / this.fixedStep;
    this.render(alpha);

    requestAnimationFrame(this.step.bind(this));
  }

  private savePreviousStates(): void { /* Archive entity positions */ }
  private updatePhysics(dt: number): void { /* Deterministic physics tick */ }
  private render(alpha: number): void { /* Interpolate and draw */ }
}

Eliminating Micro-Stutter: State Interpolation (Alpha Blending)

Separating simulation from rendering introduces a subtle visual defect: temporal aliasing or micro-stutter. Because display refresh rates (e.g. 60Hz or 144Hz) rarely divide evenly into fixed physics rates, the simulation state at the moment of rendering is slightly behind real time by the remaining fraction left in the accumulator.

Without correction, an object moving at high speed will appear to jitter slightly across frames. The solution is state interpolation. The engine archives the entity position from the previous physics tick (prevX, prevY). During the render pass, the rendering position is computed as a linear interpolation (lerp) between the previous state and the newly computed state using the remaining accumulator fraction as the blending alpha:

renderPosition = prevPosition + (currentPosition - prevPosition) * alpha

This mathematical guarantee produces silky, cinema-grade visual smoothness across all monitor hardware while keeping the underlying physics calculations 100% deterministic and reproducible.

Mitigating Background Tab Throttling & Hibernation

Modern browser engines aggressively throttle inactive tabs to prolong battery life. When a user switches tabs, requestAnimationFrame is clamped to 1 FPS or halted entirely.

If a player returns to the game tab after 30 seconds, currentTime - lastTime will register 30,000 milliseconds! Without protective clamping, the while (accumulator >= fixedStep) loop will attempt to execute 1,800 consecutive physics ticks on a single frame, completely freezing the browser tab in a catastrophic spiral of death.

Two defense layers must be implemented:

  1. Maximum Frame Time Clamping: Clamp frameTime to an upper boundary (e.g. 250ms), discarding unrecoverable time lapses.
  2. Page Visibility API Integration: Listen for document.addEventListener('visibilitychange'). When document.hidden === true, automatically transition the game state to an internal pause state. Upon reactivation, reset lastTime = performance.now() and clear the accumulator before resuming ticks.

Comparative Architectural Matrix: Game Loop Timing Strategies

Analyzing timing strategies across browser game implementations:

Loop Strategy Physics Determinism Visual Fluidity High-Refresh Adaptation Implementation Complexity
Variable Delta Time Extremely Poor (Tunneling) High on uniform displays Automatic Trivial
Fixed Step (No Interpolation) 100% Deterministic Poor (Micro-stutter / Judder) Poor (Tied to simulation rate) Moderate
Fixed Step + Alpha Interpolation 100% Deterministic Flawless (Cinema-smooth) Flawless across 60Hz–240Hz High (Requires state archiving)
Worker-Isolated Loop Absolute (Thread-isolated) Flawless (Zero main-thread lag) Flawless Very High (SharedArrayBuffer)

Conclusion

Building production-grade HTML5 Canvas and WebGL games requires abandoning variable delta time shortcuts. Implementing a fixed-timestep loop with accumulator management, delta time clamping, and alpha state interpolation guarantees mathematical determinism, prevents game-breaking collision tunneling, and delivers flawless visual fidelity across every gaming display.

Frequently Asked Questions

Why is naive requestAnimationFrame with variable delta time problematic for game physics?

When game physics equations multiply velocity by a variable delta time (dt = currentFrame - lastFrame), simulation results become inherently non-deterministic. If a browser experiences a momentary garbage collection stutter or background task spike, dt expands dramatically. In numerical integration (such as Euler integration), a massive dt causes fast-moving projectiles to completely skip collision boundaries (tunneling) or causes spring dampeners to explode into mathematical infinity.

What is the "Spiral of Death" in fixed timestep game loops, and how is it prevented?

The spiral of death occurs when physics updates take longer to compute than the fixed timestep itself. For example, if a heavy physics tick takes 25ms to calculate on a 20ms fixed step, the accumulator falls further behind each frame. The engine responds by executing multiple physics steps in the next loop, which takes even longer, completely locking the main thread. This is prevented by imposing a strict upper clamp on delta time (e.g. Math.min(frameDelta, 250ms)) to discard unrecoverable lag spikes.

How does state interpolation (alpha blending) eliminate visual micro-stutter?

Because physics steps run at a fixed frequency (e.g. 50Hz / 20ms) while display refresh rates vary across monitors (60Hz, 120Hz, 144Hz), rendering frames rarely coincide exactly with physics updates. Without interpolation, moving objects appear to judder or hitch. State interpolation calculates an alpha value representing the remaining fraction of the accumulator divided by the fixed step (alpha = accumulator / dtFixed). Renderers blend the previous physics position and current physics position (lerp(prev, curr, alpha)) to produce butter-smooth visuals.

How should web game loops handle browser background tab throttling?

When a user switches browser tabs, modern browsers throttle requestAnimationFrame callbacks, reducing execution frequency to 1Hz or halting it entirely to conserve battery and CPU resources. Upon re-focusing, the first requestAnimationFrame callback delivers a massive delta time (often several seconds). By listening to document.addEventListener("visibilitychange"), the engine can automatically pause gameplay, reset the last timestamp upon visibility restoration, and prevent catastrophic accumulator overload.

What is the performance difference between performance.now() and Date.now()?

performance.now() returns a high-resolution DOMHighResTimeStamp measured in fractional milliseconds (microsecond precision) relative to the document navigation start. Crucially, it is monotonic and unaffected by system clock adjustments. In contrast, Date.now() has low resolution (typically 1ms or coarser) and is subject to operating system clock drift and Network Time Protocol (NTP) adjustments, which can result in backwards-flowing time steps.

Build Resilient 60 FPS Web Game Architecture

Collaborate with Codeinflash interactive systems architects to optimize HTML5 Canvas loops, GPU shaders, and WebAssembly physics pipelines.

Book an Interactive Architecture Consultation →
CodeInFlash Interactive Systems Council

Written by CodeInFlash Interactive Systems Council

Senior interactive systems architect and graphics engineer specializing in HTML5 Canvas 2D game loops, WebGL shader pipelines, WebAssembly physics integration, and digital game preservation.

Need Custom Game Architecture or Graphics Advisory?

Collaborate with Codeinflash engineers to build resilient, high-speed 60 FPS interactive systems.

Book Consultation