# Chapter 3: Game loops (/guides/handmade-web-games/game-loops)





Unlike a program that runs once to completion, a game has to keep happening. For each frame while the game runs, the world state is updated and drawn to the canvas. This *game loop* continues as long as the game runs.

```ts
while (gameIsRunning) {
  update();
  draw();
}
```

But in JavaScript, a long-running `while` loop like that will just crash your tab. The game loops we'll build up to in this chapter do two things the `while` loop can't (in addition to not crashing):

1. They sync to monitor refresh rates for smooth animation.
2. They handle updates at precise fixed intervals so that physics calculations are consistent across devices.

## Basic loops [#basic-loops]

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    Since we're continuing from Chapter 2, assume all code in this chapter follows from this setup.
  </slot>

  <slot path="steps">
    <>
      For the simplest possible game loop in JavaScript, `setInterval(fn, ms)` runs a function every `ms` milliseconds. It is simple, but a poor choice for games.

      1. **The interval is not exact.** JavaScript can't guarantee your function fires every 16ms precisely, which makes it unsuitable for physics simulations that require a stable time step.
      2. **It can't sync to the monitor's refresh rate.** On a 120hz display, a 60hz interval only paints half the frames the screen is capable of. Timing mismatches between the display's refresh rate and how often the game renders will result in dropped frames. And dropped frames mean choppy animation.
      3. **`setInterval` is bad for battery life.** The interval keeps running even when the tab is hidden, draining battery for no reason.

      <Callout title="For demonstration purposes only!" type="warn">
        `setInterval` gives you the worst of both worlds--it's not precise enough for stable updates, but
        also can't sync with the monitor. Don't use it as your game loop!
      </Callout>
    </>
  </slot>

  <slot path="steps">
    <>
      `requestAnimationFrame(fn)` calls its callback `fn` just before the next browser paint, in sync with the monitor's refresh rate. To make a loop, the callback calls `requestAnimationFrame` itself to schedule its own next tick.

      <Callout type="info">
        If the work within the callback takes longer than one frame to execute, you will drop frames.

        A dropped frame is still better than the "spiral of death" that crashes your tab when code in a `setInterval` takes longer than the interval to run.
      </Callout>
    </>
  </slot>

  <slot path="steps">
    Because `requestAnimationFrame` is synchronized to the device's refresh rate, a square that moves one pixel per frame--as we saw above--will move faster on a 120hz display than on a 30hz one. So on its own, `requestAnimationFrame` does not work well as a game loop because it ties game speed to the player's hardware. But it is the right foundation to build onto.
  </slot>
</Scrollycoding>

## `deltaTime` [#deltatime]

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    To fix the varied speeds between devices with different refresh rates, the trick is to think in terms of pixels per second rather than pixels per frame. To do that, you need a way to normalize time that you can incorporate into any state that changes over time. The answer--`deltaTime`, or `dt` for short--is simply the amount of time that elapsed since the last frame.
  </slot>
</Scrollycoding>

#### deltaTime cheatsheet [#deltatime-cheatsheet]

Common uses for `deltaTime` in games include:

* **Multiplying velocity by `deltaTime` in changes to positions or angles:**

  ```ts
  player.x += player.velocity * dt;
  bullet.x += bulletSpeed * dt;
  angle += (angularSpeed * dt) % 360;
  ```

* **Applying acceleration to velocity:**

  ```ts
  player.velocity += gravity * dt;
  ```

* **Adding or subtracting `deltaTime` each tick for timers:**

  ```ts
  elapsedSeconds += dt;
  ```

  ```ts
  reloadTimer = Math.max(0, reloadTimer - dt);
  if (reloadTimer === 0) {
    canShoot = true;
  }
  ```

* **Damping / friction:**

  ```ts
  velocity *= Math.exp(-friction * dt);
  ```

* **Stepping through frames:**

  <small>
    More on this in [Chapter 4](/guides/handmade-web-games/animation) with Sprite Animation
  </small>

  ```ts
  frameTime += dt;
  if (frameTime >= frameDuration) {
    frame = (frame + 1) % totalFrames;
    frameTime -= frameDuration;
  }
  ```

* **Time-based random events:**

  Checking `Math.random()` against a fixed threshold every frame ties the event rate to the frame rate.

  <Callout title="Don't do this!" type="error">
    This fires more often on high refresh rate displays:

    ```ts title="update.ts"
    // called within `update()`
    if (Math.random() < 0.5 * dt) {
      doRandomEvent(); // 50% chance per second -- or is it?
    }
    ```
  </Callout>

  <Callout title="Better:" type="success">
    Use an accumulator that checks once per second regardless of frame rate:

    ```ts title="state.ts"
    const state = {
      eventAccumulator: 0,
      eventRate: 1, // seconds between checks
    };
    ```

    ```ts title="update.ts"
    // called within `update()`
    state.eventAccumulator += dt;
    if (state.eventAccumulator >= state.eventRate) {
      state.eventAccumulator -= state.eventRate;
      if (Math.random() < 0.5) {
        // true 50% chance, once per second
        doRandomEvent();
      }
    }
    ```

    *(Spoiler--this same accumulator pattern is how we'll add fixed timestep updates at the end of the chapter.)*
  </Callout>

*But not everything needs `dt`.*

* **Instantaneous changes do not incorporate `deltaTime`.**

  ```ts
  function jump(player) {
    player.velocityY = 100; // no dt
  }
  ```

  ```ts
  // collisions
  if (hitWall) {
    ball.velocityX *= -1; // no dt
  }

  if (player.y > groundY) {
    player.y = groundY;
    player.velocityY = 0; // no dt
  }
  ```

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    <>
      Because `requestAnimationFrame(fn)` automatically passes a high-resolution timestamp to its callback function, we can compute `deltaTime` by comparing the current frame's timestamp with the previous `lastFrame` timestamp.

      That makes `dt = (now - lastFrame) / 1000`.

      <details>
        <summary>
          <strong>Why `/ 1000`?</strong>
        </summary>

        It's usually easier to think in terms of seconds rather than milliseconds--but that's optional. Standard physics constants and equations tend to be expressed in terms of seconds, not ms, so you can do things like this:

        ```ts
        const gravity = 980; // px/s²  (not 0.98 px/ms²)
        const speed = 200; // px/s (not 0.2 px/ms)
        player.velocity += gravity * dt; // dt in seconds
        ```
      </details>

      <Callout title="Clamp your maximum delta time" type="warn">
        `requestAnimationFrame` pauses execution when the tab is hidden, which means that returning to a tab after it has been inactive for a while can create a massive spike in `dt`. This can crash your game as it tries to catch up with potentially millions of state updates.

        To prevent that in your code, pick a reasonable maximum `dt` and clamp any `dt` values that exceed it.

        Thus our `dt` calculation becomes:<br />`dt = min((now - lastFrame) / 1000, maxDt)`
      </Callout>
    </>
  </slot>
</Scrollycoding>

#### When a variable `deltaTime` is not enough [#when-a-variable-deltatime-is-not-enough]

For code that needs consistent simulations, our game loop still has a problem. With variable `dt`, the same physics code produces slightly different results depending on the frame rate. On a fast device, there are many small steps for each calculation. On a slow one, there are fewer larger steps. Those differences can cause small problems, like the game feeling slightly "off" on some devices. Or in games with fast-moving objects they can completely break collision detection. The fix is to prevent `dt` from varying at all using what's called a **fixed timestep**.

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    <>
      Before introducing the fixed timestep, we need to separate two concerns that have been mixed together in our code: updating state and drawing it. This separation is what makes the fixed timestep possible.

      <Callout type="info">
        Before we go any further, I'd like to note that the game loop we have right now is perfectly suitable for a lot of games! If you're not relying on physics simulations, stick with this simple game loop. Types of games that don't usually need fixed timesteps include:

        * Card games
        * Chess and most board games
        * Incremental games
        * Roguelikes with grid-step movement
        * Sokoban-style puzzles
        * Visual novels

        Put another way, if the state in your game is discrete rather than a continuous simulation of time, the game loop on the right is a great choice.
      </Callout>
    </>
  </slot>

  <slot path="steps">
    <>
      To achieve the precise updates needed in physics-based simulations and games, we'll introduce a fixed timestep. The idea is to decouple the draw cycle from the physics update cycle. This means you can process physics updates on an *exact* interval--far more precise than the approach with `setInterval`--while also only painting to the screen in time with its current refresh rate.

      How? To reference the classic article, [Fix Your Timestep!](https://gafferongames.com/post/fix_your_timestep/) by Glenn Fiedler:

      {/* prettier-ignore */}

      <blockquote className="font-fancy italic">
        <q>
          Instead of thinking that you have a certain amount of frame time you must simulate before rendering, flip your viewpoint upside down and think of it like this: the renderer produces time and the simulation consumes it in discrete 

          `dt`

           sized steps.
        </q>
      </blockquote>

      Every frame, we accumulate real elapsed time in a variable (`accumulator` in the code to the right). The loop of `fixedUpdate` calls drains it in fixed amounts. These new delta times are guaranteed to be the same every single time and on every device. And your renderer still runs at whatever rate the display supports.

      <details>
        <summary>
          <strong>Taking the draw loop one step further with interpolation</strong>
        </summary>

        You'll usually have a tiny bit of accumulated time leftover before you draw. This means your draw might occasionally be slightly behind where it should be--especially at lower frame rates.

        To solve this, [Fix Your Timestep](https://gafferongames.com/post/fix_your_timestep#the-final-touch) suggests interpolating your draws between the current and next physics states based on how much time is left in the accumulator. The idea is to figure out what percent of the next physics step you are currently into, and incorporate that into your draw code.

        The downside is that rather than saving just one state value for each physics property, you now need to save two--the current and previous values. Any logic that previously could simply read `state.x` will now need to calculate `interpolate(state.prevX, state.x, alpha)`, which is a tradeoff in complexity you will need to weigh for your specific game. With modern devices supporting 120-240fps as a baseline frame rate, interpolation is rarely worth the effort these days.
      </details>
    </>
  </slot>
</Scrollycoding>

## Complete fixed timestep example [#complete-fixed-timestep-example]

The complete example below combines everything from this chapter with the DPI scaling boilerplate from Chapter 2. It also includes a `ResizeObserver` that re-runs the DPI scaling whenever the canvas changes size. The `index.html` is the same full-window canvas setup from [Chapter 1](/guides/handmade-web-games/tooling-for-web-games-in-2026). In addition to the fixed timestep `fixedUpdate` function, we'll also include a variable-rate `update` function that's useful for visual effects like particles or sprite animation that normally don't require stable physics and can run with a variable `dt`.

<Files>
  <File name="index.html" />

  <Folder name="public">
    <File name="favicon.svg" />
  </Folder>

  <Folder name="src">
    <File name="main.ts" />
  </Folder>
</Files>

<CodeBlockTabs defaultValue="src/main.ts">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="src/main.ts">
      src/main.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="index.html">
      index.html
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="src/main.ts">
    ```ts  twoslash
    const canvas = document.createElement("canvas");
    document.body.appendChild(canvas);
    const ctx = canvas.getContext("2d")!;
    const initialBounds = canvas.getBoundingClientRect();

    const state = {
      x: 0, // replace this with your game's state
      bounds: {
        width: initialBounds.width,
        height: initialBounds.height,
      },
    };

    // update with a stable `dt`
    function fixedUpdate(dt: number) {
      const { bounds } = state;
      state.x += dt;
      if (state.x > bounds.width) {
        state.x = 0;
      }
    }

    // update with a variable `dt`
    function update(dt: number) {}

    function draw(ctx: CanvasRenderingContext2D) {
      const { width, height } = state.bounds;
      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = "deepskyblue";
      ctx.fillRect(state.x, 80, 20, 20);
    }

    const fixedDt = 8 / 1000;
    let lastFrame = 0;
    let accumulator = 0;

    function loop(now: number) {
      if (!lastFrame) lastFrame = now;
      const elapsed = now - lastFrame;
      const dt = Math.min(elapsed / 1000, 0.1);
      accumulator += dt;
      lastFrame = now;
      while (accumulator >= fixedDt) {
        fixedUpdate(fixedDt);
        accumulator -= fixedDt;
      }
      update(dt);
      draw(ctx);
      requestAnimationFrame(loop);
    }

    updateCanvasSize();
    requestAnimationFrame(loop);

    new ResizeObserver(updateCanvasSize).observe(canvas);

    function updateCanvasSize() {
      const { width, height } = canvas.getBoundingClientRect();
      const dpr = window.devicePixelRatio;

      // set how many pixels exist within the canvas
      canvas.width = width * dpr;
      canvas.height = height * dpr;

      // update logical bounds, in css pixels
      state.bounds.width = width;
      state.bounds.height = height;

      // ensure consistent scaling
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      // set css layout size
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="index.html">
    ```html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Your Game</title>
        <style>
          canvas {
            position: fixed;
            inset: 0;
            width: 100%;
            height: 100%;
          }
        </style>
      </head>
      <body>
        <script type="module" src="/src/main.ts"></script>
      </body>
    </html>
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Up next [#up-next]

With a game loop in place and the drawing techniques you learned in Chapter 2, we now have all the tools needed to start animating. Sprites, easing curves, squash/stretch techniques, and ways to blend between states follow in Chapter 4.

## See also [#see-also]

* [DeltaTime explained by Jonas Tyroller](https://www.youtube.com/watch?v=yGhfUcPjXuE) <small className="opacity-75">\[YouTube]</small>
* [Fix Your Timestep!](https://gafferongames.com/post/fix_your_timestep/) — the original C++ article by Glenn Fiedler
* [The JavaScript Event Loop explained by Lydia Hallie](https://www.youtube.com/watch?v=eiC58R16hb8) <small className="opacity-75">\[YouTube]</small>
