A loop in a roller coaster

Create a proper JavaScript game loop

Build a smooth JavaScript game loop for the canvas with requestAnimationFrame. Learn about delta time and fixed timesteps, and measure fps with live demos. By the end of this tutorial, you'll have a complete game loop ready to use in your own game.

Why do you need a game loop?

In the tutorial on setting up an HTML5 canvas, you wrote a draw() function that draws a rectangle. That's super nice and all, but that drawing operation is only performed once. If you want to make it appear as if the rectangle is moving, and eventually create an actual game, you need to draw more. A lot more.

To achieve this, you'll need a loop to repeatedly execute your game logic. In the world of games, this loop is called a game loop. It'll be the core of your game and trigger the drawing operations. Instead of a single static image, you'll draw many images one after another, which creates motion.

Here's an example with a moving circle. Each frame shows the circle in a slightly different position, and every frame is one iteration of your game loop. At 2 frames per second, or 2 fps, you can still see the individual frames. The dashed circles in the demo below show where the circle was in the previous frames. Switch to 60 fps, and the small steps follow each other so quickly that they create the illusion of motion.

Frame rate

The benefits of a high frame rate

As you might know, when it comes to games, people talk about how many fps they can get out of their gaming rig. Fps stands for frames per second. Each time you draw your game objects on the screen counts as a frame.

Fps is the number of times per second you can draw your game on the screen. In general, the better your hardware, the higher your fps will be. And the higher your fps, the smoother your game will play. Also, the delay between frames will get shorter, making the visible state of the game more up to date. This can give you an edge in the game. That's why professional gamers want to squeeze out every frame they can get and spend a lot on the best possible hardware.

A professional gamer

The ideal frame rate for your game loop

Humans can see about 10 to 12 frames per second as separate images. Any more frames than that and you will perceive it as motion, and that is what you want for your game. Of course, a game running at 12 fps would be extremely choppy. You might see it as motion, but you need more frames if you want to make it look smooth. Movies generally use 24 fps, but most games run at a much higher frame rate than that. If more is better, why not just aim to run your game at 240 fps and make it ultra-smooth?

Well, there is also the refresh rate of your display to take into account. A display shows a new image a fixed number of times per second, measured in hertz (Hz). For years, 60 Hz was the standard. These days a lot of phones and laptops run at 90 or 120 Hz, and gaming monitors go up to 144 Hz, 240 Hz or even higher.

Running a game at 240 fps on a 60 Hz display won't do much good. You'll have more frames to draw than your display can show. The extra frames will never be displayed and only cost you system resources. And an HTML5 game isn't just for high-end desktop rigs with great displays. It probably has to run and perform well on mobile devices too.

A frame rate that's equal to the refresh rate of your display would be ideal. It offers the smoothest experience and keeps the game state as up to date as possible. But you also don't want your game to use more system resources than it needs or drain a phone's battery.

So, to sum it all up, this is what you'll be looking for in a game loop for an HTML5 game:

  • Smooth - The frame rate is as high as possible.
  • In sync with the screen - The game draws a frame right before each screen refresh, and no more often than that.
  • Light on resources - The game doesn't use more system resources than it needs.

Let's check out some options and see how well they fit these requirements. You are going to find the perfect loop for your game and learn about common mistakes.

Set up a game loop

OK, so you need a game loop? Why not just use this simple while loop in your JavaScript code?

// A bad game loop
while (running) {
    draw();
}

The loop just starts up and keeps on drawing, as fast as your hardware allows. So you'd get the highest fps possible, right?

But there is a small problem. JavaScript runs your code on a single thread, the main thread of the browser tab. With the while loop, all of that thread's time goes into the drawing operations, again and again. The browser only updates the screen when your code is done, and this loop is never done. So you never see a single frame of what you've drawn. The browser can't handle user input or other important events either, so not even a pause button can stop the loop. If you let it run for a while, it will hang your browser and eventually trigger the dreaded "this page is not responding" warning.

To fix this, you'll need to give the browser some breathing room while the game loop runs. You could use something like setInterval() to call the game loop at a fixed interval. This puts some time between drawing operations, giving the browser space to perform tasks other than drawing.

// Another bad game loop
setInterval(gameLoop, 16);

function gameLoop() {
    draw();
}

With an interval of 16 ms, this game loop will reach about 60 fps. This will perform better than the while loop, but the timer doesn't know when the browser updates the screen. That moment is called a repaint. An interval of 16 ms is a bit shorter than the 16.7 ms between two repaints on a 60 Hz screen, so the timer and the repaints slowly drift apart. You might calculate frames that never make it to the screen, or miss a repaint now and then, which makes the motion stutter. The interval doesn't know the refresh rate of your screen either. On a 120 Hz screen, the loop still runs at 60 fps. A gameLoop() that starts a new setTimeout() at the end of every frame has the same problems. So how do you fix this?

The proper JavaScript game loop

You need a way to loop without blocking the browser and to keep your game loop in sync with the browser's repaints. Luckily, there's a solution. You can use window.requestAnimationFrame() to tell the browser you want to update an animation, or in this case a game, before the next repaint.

// The proper game loop
window.requestAnimationFrame(gameLoop);

function gameLoop() {
    draw();
    window.requestAnimationFrame(gameLoop);
}

The browser will call your callback function, the gameLoop function you passed to requestAnimationFrame(), right before it draws the next frame on the screen. It might look like gameLoop() calls itself forever, just like the while loop. But requestAnimationFrame() doesn't call gameLoop() right away. It only asks the browser to call gameLoop() before the next repaint, and then returns. So gameLoop() finishes, and the browser has time to update the screen and handle other events. And when a browser tab is hidden, most browsers pause the callbacks completely. This saves system resources and battery life.

Remember the section about the ideal frame rate? With requestAnimationFrame(), the browser picks a suitable frame rate for the device it's running on. The callbacks follow the refresh rate of the display: about 60 times per second on a 60 Hz screen, 120 times on a 120 Hz screen, and so on.

requestAnimationFrame() meets all the requirements for a proper game loop. The loop runs at a frame rate that makes the game look smooth, takes the refresh rate of the screen into account and doesn't use more system resources than it needs. Here's a quick overview of the options you've seen so far:

  • while loop - Blocks the browser. Nothing is shown on the screen and the page stops responding.
  • setInterval() or setTimeout() - Keeps the browser responsive, but runs on its own schedule. Frames get wasted or skipped and the refresh rate of the screen is ignored.
  • requestAnimationFrame() - Runs right before each repaint, follows the refresh rate and pauses in a hidden tab. The perfect match for your own HTML5 game.

How to use requestAnimationFrame()

The function window.requestAnimationFrame() accepts a callback function as an argument. You can pass your gameLoop() function in there. When the browser is ready, it will execute that function.

You will have to start the game loop by calling window.requestAnimationFrame() once, and then keep on calling it inside the loop. It's easier to understand with an example. Here's the script from the tutorial on setting up a canvas, with a game loop added:

<script>
    'use strict';
    let canvas;
    let context;

    window.onload = init;

    function init() {
        // Get a reference to the canvas
        canvas = document.getElementById('canvas');
        context = canvas.getContext('2d');

        // Start the first frame request
        window.requestAnimationFrame(gameLoop);
    }

    function gameLoop(timeStamp) {
        draw();

        // Keep requesting new frames
        window.requestAnimationFrame(gameLoop);
    }

    function draw() {
        // Pick a random color, red or blue
        let randomColor = Math.random() > 0.5 ? '#ff8080' : '#0099b0';

        // Draw a rectangle
        context.fillStyle = randomColor;
        context.fillRect(275, 125, 200, 150);
    }
</script>

The callback you pass to requestAnimationFrame() gets one argument, a timestamp in milliseconds. In the code above, that's the timeStamp parameter of gameLoop(). It's measured on the same clock as performance.now(), which returns the number of milliseconds since the page started loading. You might expect it to be the moment your callback runs, but it's the moment the browser finished the previous frame. That's why it's close to what you would get from calling performance.now() yourself, but not exactly the same.

You won't be doing anything with the timestamp right now. Further down this page you'll use it to measure the fps and to keep the speed of your game the same on every screen. In the next tutorial, on canvas animations, you'll also use it for easing.

Check out the running game loop

Let's try out your new game loop. When you run this code, you'll see a big difference compared to the tutorial on setting up a canvas, where draw() was called only once. The draw() function is now called many times in a row. The rectangle is redrawn roughly 60 times per second, or more on a screen with a higher refresh rate.

Here's a diagram of your game loop. For now, it's a pretty simple one with a single step, draw, that repeats every frame:

Most game loops end up doing three things every frame, always in the same order. First they handle the player's input, then they update the game objects, and as a final step they draw everything to the screen. That way the screen always shows the latest state of your game. Right now your loop only draws, but later tutorials in this series add the other tasks one by one. Player input, for example, is added in the tutorial on keyboard, mouse and touch controls.

The color of the rectangle is still random, and a new one is picked every frame. That's why you now see a flickering rectangle that switches rapidly between red and blue. This makes it easier to see that the game loop is actually working. The flickering isn't very pleasant to look at, though.

Measure and display fps

Well done, you've made a game loop and it's working. But wouldn't it be nice if you could measure its performance and check out how many frames per second it produces? Let's try to calculate the current fps by using the timeStamp parameter provided in the game loop.

First, calculate the time that has passed since the previous frame. You can do that by subtracting the timestamp of the previous frame from the current timestamp. The result is the number of milliseconds since the last frame.

Divide this number by 1000 to go from milliseconds to seconds. This is the number of seconds it takes to produce one frame. Divide 1 by this number to get the number of frames per second.

Here is an example of a very simple implementation that uses a fillText() drawing operation, as explained in the canvas drawing tutorial, to draw the fps as text to the screen:

let oldTimeStamp = 0;
let fps = 0;

function gameLoop(timeStamp) {
    // Calculate the number of seconds passed since the last frame
    let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
    oldTimeStamp = timeStamp;

    // Calculate fps
    fps = Math.round(1 / secondsPassed);

    // Paint over the old number with a white rectangle
    context.fillStyle = 'white';
    context.fillRect(0, 0, 200, 100);

    // Draw the new number on the screen
    context.font = '25px Arial';
    context.fillStyle = 'black';
    context.fillText('fps: ' + fps, 10, 30);

    // Perform the drawing operation
    draw();

    // The loop function has reached its end. Keep requesting new frames
    window.requestAnimationFrame(gameLoop);
}

The fps is updated every frame, so the number keeps jumping around and is hard to read. A better way is to count the frames and update the number once every second. Keep oldTimeStamp and fps from the example above, add two new variables and change gameLoop() like this:

let frameCount = 0;
let fpsTimer = 0;

function gameLoop(timeStamp) {
    let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
    oldTimeStamp = timeStamp;

    // Count the frames and update the fps once per second
    frameCount++;
    fpsTimer += secondsPassed;
    if (fpsTimer >= 1) {
        fps = Math.round(frameCount / fpsTimer);
        frameCount = 0;
        fpsTimer = 0;
    }

    // Draw the fps and the rectangle, like before
    // ...

    window.requestAnimationFrame(gameLoop);
}

Every frame, frameCount goes up by one and fpsTimer adds up the time that has passed. As soon as a full second has passed, the fps is the number of frames divided by that time, which is usually a tiny bit more than one second. Then both are reset to 0.

Here is the result. If you view this page on a desktop device with a regular screen, it should hit a nice 60 frames per second. On a phone or a gaming monitor you might see 90, 120 or even more.

Start, pause and stop the game loop

Right now, your game loop runs forever. In a real game you'll want to pause it, for example when the player opens a menu, and stop it when the game is over.

Each call to requestAnimationFrame() returns an ID for the frame you've requested. Pass that ID to cancelAnimationFrame() and the request is canceled. The callback never runs, so the loop stops. Here's an example:

let frameId;

function startLoop() {
    frameId = window.requestAnimationFrame(gameLoop);
}

function pauseLoop() {
    window.cancelAnimationFrame(frameId);
}

function gameLoop(timeStamp) {
    draw();

    // Keep requesting new frames and remember the ID
    frameId = window.requestAnimationFrame(gameLoop);
}

Make sure you store the ID every time you request a new frame. If you don't, you'll cancel an old request, and the loop will just keep running. Stopping the game works the same way, but you also reset the game.

There's one thing to watch out for when you start the loop again after a pause. The first frame after the pause comes a long time after the one before it. Right now, that only makes your fps meter show the wrong number, but once your game objects move based on time, as they will in the next section, they'll jump ahead. And there's a similar problem with the very first frame you draw on your canvas. The oldTimeStamp is still 0, so it counts all the time since the page started loading. You can fix both by having the first frame only remember its timestamp:

function startLoop() {
    // The first frame only sets the clock
    frameId = window.requestAnimationFrame(firstFrame);
}

function firstFrame(timeStamp) {
    oldTimeStamp = timeStamp;
    frameId = window.requestAnimationFrame(gameLoop);
}

From the second frame on, the loop measures the time like before.

Same speed at 60 Hz and 144 Hz

Remember that requestAnimationFrame() follows the refresh rate of the display? That's great for smoothness, but it has a side effect. If you move a game object a fixed number of pixels each frame, it moves twice as fast on a 120 Hz screen as on a 60 Hz screen. On a 144 Hz gaming monitor it's even faster.

Imagine a racing game where the cars move 2 pixels per frame. A player with a fancy new laptop would suddenly have the fastest car in the race. Not very fair.

The fix is to use time instead of frames. You already know how much time has passed since the last frame: the secondsPassed from the fps meter. This is often called delta time. Multiply the speed of your objects by it and they move the same distance per second, no matter how many frames you get. Do this in a new update() function. Your game loop will call it every frame, right before draw(), and pass it secondsPassed:

let rectX = 0;
let movingSpeed = 120;

function update(secondsPassed) {
    // Move 120 pixels per second, no matter the frame rate
    rectX += movingSpeed * secondsPassed;
}

Then use rectX as the x position of the rectangle in draw(). Clear the canvas first with clearRect(), or all the old rectangles will stay visible.

At 60 fps, secondsPassed is about 0.0167, so the rectangle moves 2 pixels per frame. At 120 fps it's half of that, 1 pixel per frame, but twice as often. After one second, both have moved 120 pixels.

Try it out in the demo below. It simulates the frame rate you pick, so you can see all three on any screen. The top block moves a fixed distance each frame, while the bottom block moves based on time. Switch between the frame rates and see what happens:

Frame rate

There's one catch. When a player switches to another tab, the browser pauses the loop on its own. firstFrame() doesn't help here, because your code never stopped the loop. So when the player comes back, secondsPassed can be several seconds and all your game objects make a giant leap. To prevent this, limit the delta time to a maximum, for example 0.1 seconds, right after you calculate secondsPassed:

// Cap the delta time at 0.1 seconds
secondsPassed = Math.min(secondsPassed, 0.1);

Use a fixed timestep for physics

Delta time works fine for most animations. But once you add physics and collision detection, a delta time that changes every frame can cause trouble. With a big delta time, a fast ball moves so far in one frame that it can pass straight through a wall without ever touching it. And a jump can end up a little higher on one computer than on another, because the math gives slightly different results for different delta times.

Physics engines solve this with a fixed timestep. The game logic always moves forward in the same small steps, for example 1/60 of a second, no matter the frame rate. The loop uses an accumulator to keep track of the time that still needs to be simulated:

const fixedStep = 1 / 60;
let accumulator = 0;
let oldTimeStamp = 0;

function gameLoop(timeStamp) {
    let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
    oldTimeStamp = timeStamp;

    // Catch up by at most 0.1 seconds per frame
    secondsPassed = Math.min(secondsPassed, 0.1);

    // Add the time that has passed to the accumulator
    accumulator += secondsPassed;

    // Update in fixed steps, as many as fit in the accumulated time
    while (accumulator >= fixedStep) {
        update(fixedStep);
        accumulator -= fixedStep;
    }

    draw();
    window.requestAnimationFrame(gameLoop);
}

Each frame, the time that has passed is added to the accumulator. Then update() runs once for every whole step that fits in it. On a 60 Hz screen that's usually once per frame. On a 120 Hz screen it's about every other frame, and on a slow device it can be two or three times in a single frame.

The Math.min() line protects you from the spiral of death. If the device can't keep up, every frame has more updates to do, which makes the next frame even slower, and so on, until the game freezes. With a cap in place, any time above 0.1 seconds is dropped, so the game just slows down a bit instead. A limit of 0.1 seconds is a sensible choice, but the best value depends on your game. With a higher limit, the game catches up more after a hiccup, but your objects also make a bigger jump.

Here's the accumulator in slow motion, over 100 milliseconds. The top row shows the frames on the screen, and the bottom row shows the game updates, 60 per second. Every update that has run gets a check mark, and the red part is the time that's still in the accumulator. Change the refresh rate to see how many updates run in each frame:

Refresh rate

What's left in the accumulator is part of a step that hasn't been simulated yet. On a screen with a high refresh rate, the motion can look a bit jerky, because the objects only move 60 times per second while the screen shows more frames. You can smooth this out with interpolation. Draw every object between its previous and its current position, using the leftover time:

// How far we are between the last update and the next one (0 to 1)
let stepProgress = accumulator / fixedStep;
draw(stepProgress);

In draw() you then use previousX + (rectX - previousX) * stepProgress as the x position, instead of rectX. At the start of every update(), save rectX in previousX. The rectangle is then drawn up to one step behind its latest position, but at 60 updates per second, that's hard to notice. Interpolation only changes where the object is drawn. The game itself still moves in fixed steps in update(), so collisions and physics work the same on every screen. The demo below updates a bouncing ball only 10 times per second, so you can clearly see the difference. Turn interpolation on and off:

Interpolation

The idea of a fixed timestep with interpolation comes from the article Fix Your Timestep! by Glenn Fiedler, a classic among game developers. Do you need a fixed timestep for every game? Not really. If your game doesn't rely on precise physics, delta time is fine. For collisions and physics, a fixed timestep makes life a lot easier.

Pause the game when the tab is hidden

Browsers pause requestAnimationFrame() in a hidden tab, but the rest of your game doesn't know that. Music keeps playing, a countdown that uses the clock keeps running, and when the player returns, the delta time is huge. With the Page Visibility API you can find out when the page is hidden and pause the game yourself:

document.addEventListener('visibilitychange', function () {
    if (document.hidden) {
        pauseGame();
    } else {
        resumeGame();
    }
});

In pauseGame() you can stop the loop with cancelAnimationFrame() and mute the sound. In resumeGame() you start the loop again with startLoop(), so the first frame only remembers its timestamp. Or show a pause menu instead of resuming right away, so the player can continue when they're ready. Want a pause menu with buttons and a game that keeps drawing while it's paused? See the JavaScript game states tutorial.

The loop stops by itself in a hidden tab, but the sound keeps playing. See how to pause the audio in the JavaScript game audio tutorial.

Should you limit the frame rate?

Most of the time, you don't have to limit the frame rate. Let requestAnimationFrame() follow the screen and use delta time or a fixed timestep for the speed of your game. But sometimes there's a good reason, for example to save battery in a simple puzzle game that doesn't need 120 fps.

You can skip frames until enough time has passed:

const fpsLimit = 30;
const frameInterval = 1000 / fpsLimit;
let lastDrawTime = 0;

function gameLoop(timeStamp) {
    window.requestAnimationFrame(gameLoop);

    // Skip this frame if it's too soon
    let elapsed = timeStamp - lastDrawTime;
    if (elapsed < frameInterval) {
        return;
    }

    // Keep the leftover time, so the rhythm stays even
    lastDrawTime = timeStamp - (elapsed % frameInterval);

    update();
    draw();
}

This time, gameLoop() requests the next frame at the top. Otherwise, the early return would skip that request too, and the loop would stop.

The line with the leftover time matters. The time between frames is never exactly the same. That's why lastDrawTime is set to when the frame should have been drawn, not to when it actually was. Without that line, a frame that's a tiny bit early gets skipped, and a 30 fps limit on a 60 Hz screen ends up at about 24 fps. Also pick a limit that fits the refresh rate of the screen. 30 fps works well on 60, 90, 120 and 240 Hz screens, because those are all multiples of 30. 144 isn't, so the loop switches between every fourth and fifth refresh, and the motion gets a little uneven.

The tutorial on Conway's Game of Life slows down its loop in a different way. It uses a setTimeout() to wait before requesting the next frame, so you can follow each step of the simulation. That works fine there, because nothing in the Game of Life moves smoothly: every step is a new picture. The point being, there's no single right way to slow down your loop. Pick the solution that fits your project.

Common questions about game loops

Should I use setInterval() or requestAnimationFrame() for a game loop?

Use requestAnimationFrame(). It runs in sync with the screen, follows its refresh rate and pauses in a hidden tab. setInterval() and setTimeout() run on their own schedule, which wastes frames and can make the motion stutter.

Why does my game run faster on a 120 Hz or 144 Hz screen?

Because requestAnimationFrame() calls your game loop once for every screen refresh. If you move your objects a fixed amount per frame, they move faster when there are more frames. Use the delta time instead, as explained above.

Does requestAnimationFrame() run in a background tab?

No, in most browsers it's paused when the tab is hidden. If your game runs in an iframe, for example when it's embedded on another site, it can also be slowed down while the iframe is out of view. Limit the delta time, so your game doesn't jump ahead when the player comes back.

How many fps does a game need?

There's no fixed number. 30 fps is playable, 60 fps feels smooth for most games, and fast action games benefit from more. With requestAnimationFrame() you get what the screen supports, and many screens still run at 60 Hz. So make sure your game also plays well at 60 fps.

How does the game loop work in PixiJS or three.js?

Libraries like these run the game loop for you, on top of requestAnimationFrame(). PixiJS has a ticker that calls your function every frame and tells you how much time has passed. In three.js you pass your function to renderer.setAnimationLoop(). The ideas on this page, like delta time and a fixed timestep, still apply.

The complete game loop

Here's everything put together: a game loop with a fixed timestep for the game logic, interpolation for smooth motion, a cap on the delta time and an fps meter. You can start, pause and stop it with the buttons, and it pauses by itself when the tab is hidden. Use it as a starting point for your own game:

What's next?

That's it for game loops for now. Your JavaScript game loop is in place and the rectangle gets redrawn many times per second. You understand the relationship between fps and a game loop, you can measure the fps of your own game and you know how to make your game objects move at the same speed on every screen. If you have any questions, feel free to ask them in the comment section. You can