Control the Frame Rate of requestAnimationFrame Callbacks

Content:

The window.requestAnimationFrame() function is incredibly useful for creating animations in JavaScript. By passing a callback to this function, you’re telling the browser to run the specified function when it next repaints the screen.

To learn more about requestAnimationFrame(), take a look at our introductory article on the function.

Browser repaint frequency will generally match the refresh rate of your monitor, which is likely to be at least 60 frames per second (fps).

A downside to drawing on a canvas element is the extra load that is placed on the system – most of which is at the drawing phase. Painting your canvas to the screen at such a high frame rate can cause stuttering due to the high load – especially if you’re using shadows or opacity.

This article will show you how to reduce the frame rate used to draw to a canvas, reducing the load placed on the system.

Preparing Your Code

The first thing to do is to check that your processing and drawing code are separate. This is not only good practice, but it means they can run at different rates.

The goal is to end up with something structured like this:

function update() {
  // update simulation
}

function draw() {
  // draw result
}

function run() {
    update();
    draw();

    requestAnimationFrame(run);
}

run();

The run() function acts as the application entry point. Inside, we first run our update() function, which does our back-end processing, such as generating new coordinates. Next, we run draw(), which actually draws our elements to the screen.

Here’s a simple example, to draw a 20×20 box on the screen at a random location within the canvas.

let x = y = 0;

function update() {
    x = Math.floor(Math.random() * canvas.width);
    y = Math.floor(Math.random() * canvas.height);
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.rect(x-10, y-10, 20, 20);
    ctx.strokeStyle = 'black';
    ctx.stroke();
}

We’ll add a run function, which will run these two functions in turn.

function run() {
    update();
    draw();

    requestAnimationFrame(run);
}

run();

With this code, we’re still running everything on each requestAnimationFrame call, as the entire run function will be executed.

Adding Our Frame Rate Limiter

We’ll leave our update function to run on each call, but limit our draw function to a maximum of 30fps.

To do this, we’ll first add a variable which will keep track of the time passed since the function last executed.

let lastDrawTime = Date.now();

function run() {
    update();
    draw();

    lastDrawTime = Date.now();
    requestAnimationFrame(run);
}

Next, we’ll check whether the required time has passed to meet our target frame rate. To work this out, divide 1000 by your target frame rate. For 30 fps, this would be 1000/30, which gives us 33.33ms per frame.

We’ll hard code this for now, but you might want to split this off into a variable later on.

function run() {
    update();
    draw();

    if (Date.now() - lastDrawTime > 33.33) {
        // Time has passed
    }

    lastDrawTime = Date.now();
    requestAnimationFrame(run);
}

Now we can move our draw call inside the loop, along with the code we added to update lastDrawTime.

function run() {
    update();

    if (Date.now() - lastDrawTime > 33.33) {
        draw();
        lastDrawTime = Date.now();
    }

    requestAnimationFrame(run);
}

Now, on each requestAnimationFrame() call, we’re still executing the run function. Our update function will run every time. But now, our check will ensure that the target frame time has passed, before the draw call will be executed.

To verify this is working, add a console.log statement both inside, and outside of the if() statement. Assuming your default callback rate is 60 per second, you should see that there are two console.log results from the call outside the if(), for every one call inside.

What to Watch Out For

There are a couple of things to consider when using this technique.

Fluctuating Callback Rates

You might have noticed that you didn’t always get that 2:1 ratio of log results.

The callback rate is likely to fluctuate slightly – this is completely normal. But if our callbacks execute fractionally too fast, our second run will still be below our target.

E.g.

1000 / 60 = 16.67 // 60fps callback rate

Call 1: 16.65ms
Call 2: 16.66ms
Total: 33.31ms

That’s still below 33.33ms on the second call, meaning it will actually take another call for our draw function to execute, giving an effective draw rate in our canvas of 20fps.

I intentionally rounded our previous result to 2 decimal places to demonstrate how a tiny fluctuation can prevent our code from running as we intended. And if you’re calculating the target draw rate directly, there will be no rounding at all.

For this reason, it’s worth allowing a small amount of leeway to our frame time figure. Reducing it by 5% would give a target of 31.66ms, which would have lead to our example being executed. I’d encourage testing different values to see the impact on your code. Just remember that fluctuations are expected, and your frame rate will occasionally differ from the target as a result.

Choosing a Target Frame Rate

When choosing a frame rate, make sure it’s a factor of 60. This is important as we’re still relying on requestAnimationFrame() to run our code. This will most likely be running 60 times a second, as mentioned in the opening of this article.

Targeting a non-factor, such as 40fps (25ms frame time) would still take 2 calls trigger our draw code. Instead, we’ll be overshooting our target each time.

1000 / 60 = 16.67 // 60fps callback rate

Call 1: 16.67ms
Target Reached: 25ms
Call 2: 33.34ms

Overshoot: 13.34ms

In reality, the draw rate will be around 30fps.

You can’t use this to increase the frame rate either, for the same reason.

If you like what we do, consider supporting us on Ko-fi