The KIRUPA orange logo! A stylized orange made to look like a glass of orange juice! Tutorials Coding Exercises Videos Books

FORUMS

Customize Theme


Color

Background


Done

Fixing the Frame Rate When Using requestAnimationFrame

by kirupa   |   filed under Web Animation

When we looked at requestAnimationFrame, we mentioned that the speed it runs is tied to how fast the browser decides to do a repaint. This speed is usually around 60 times a second, which matches the refresh rate (60Hz) digital screens typically run at. Many of us these days are on screens with faster refresh rates. Beyond refresh rate, there are a few other factors that impact the speed that requestAnimationFrame runs at:

  1. Whether the browser supports high refresh rates
  2. The browser is too busy doing something more important where a repaint takes lower priority
  3. The browser is minimized, the browser tab loses focus
  4. The device is optimizing for battery/cpu

Why am I telling you all this? I’m telling you this because when requestAnimationFrame runs at different speeds for different people, the final result is an animation that will be either too fast or too slow. See below for an example of the same requestAnimationFrame code running on a 120Hz screen and a 60Hz screen in Chrome:

Notice the very different speeds the animation is running at. The same animation on a 120Hz screen runs twice as fast as the same animation viewed on a 60Hz screen. To put it lightly, such inconsistency isn’t desirable! This problem with our requestAnimationFrame-based animations running at varying speeds isn’t a new one, but it is definitely more pronounced these days with more and more of us having devices that are capable of displaying frame rates higher than the 60fps that was the norm for so long.

In this article, we’ll look at two approaches to adapt our animation-related code to ensure it runs at a consistent speed independent of what the screen refresh speed is set to. This is going to be a hoot!

Onwards!

Approach #1: Fixing the Frame Rate

One approach for addressing this problem is to fix the frame rate. For example, let us say we want to animate at 60fps. This means that every 16.67 milliseconds (1 second / 60 frames) aka the frame interval, we will run our code that will make a visual update. The code for doing this looks as follows:

// set the expected frame rate
let frames_per_second = 60;

let interval = Math.floor(1000 / frames_per_second); // rounding down since our code will rarely run at the exact interval
let startTime = performance.now();
let previousTime = startTime;

let currentTime = 0;
let deltaTime = 0;

function animationLoop(timestamp) {
  currentTime = timestamp;
  deltaTime = currentTime - previousTime;

  if (deltaTime > interval) {
    previousTime = currentTime - (deltaTime % interval);

    // add your visual updates-related code
  }

  requestAnimationFrame(animationLoop);
}
requestAnimationFrame(animationLoop);
					

If we look at our code, the way we fix the frame rate is by using our requestAnimationFrame as nothing more than a timing loop and checking the amount of time elapsed (commonly known a the delta time) between each requestAnimationFrame call:

For 60fps, if the elapsed time is less than 16.667 milliseconds (1 second / 60 frames), we wait until the next requestAnimationFrame call and check again. We keep checking until the amount of time elapsed is equal or greater than 16.67 milliseconds and run our visual update code if enough time has elapsed. With this approach, it doesn’t matter what speed requestAnimationFrame is running at. We only care about the elapsed time. The code that is relevant for updating our visuals will only run every 16.667 milliseconds, so we are fully decoupled from the many factors that can lead to higher or lower raF speeds.

If we specify a frame rate that is greater than what the browser/screen can handle, our code will run at whatever maximum speed requestAnimationFrame is capable of. For example, if we set our frame rate at 1000, our frame interval would be 1 millisecond (1 second / 1000 frames). Unless we are OK with some users seeing a faster or slower version of our animation code, such a high frame rate would result in both wasted work and users seeing the animation at whatever inconsistent maximum requestAnimationFrame speed their browser is capable of.

Keeping the frame rate set to 60 is a safe (and good!) choice given its status as the unofficial historical default frame rate requestAnimationFrame ran at!

Approach #2: Using a Delta Time Multiplier

Another approach to ensure our animations run at a consistent speed involves using what is known as a delta time multiplier. We saw earlier that the delta time is the interval of time between each requestAnimationFrame update. For a 60Hz refresh rate, our delta time would be 16.667 milliseconds (1 second / 60Hz). For a 120Hz refresh rate, our delta time would be 8.333 milliseconds (1 second / 120Hz):

We then have our frame interval, which is our desired frame rate divided by 1 second. For a desired frame rate of 60fps, our frame interval is 16.667 milliseconds. If our requestAnimationFrame runs on a 60Hz screen, we saw earlier that the delta time conveniently happens to be 16.667 milliseconds as well. In this situation, we have a 1-to-1 consistency beteeen our frame and delta times. This means our delta time multiplier is 1.

The general formula for calculating the delta time multiplier is:

Using a 120Hz screen as our example, the delta time is 8.333 milliseconds. For a desired frame rate of 60fps where the frame interval is 16.667 milliseconds, the delta time multiplier is a 0.5. Now, why am I telling you all of this? What does the delta time multiplier have to do with ensuring our animations run at a consistent speed? Hold this thought. There is an additional detail we are going to look at.

Going all the way back to the beginning, we said that an animation is a visualization of change:

the interpolated values

Over a period of time, our animation will go from a starting state to an ending state. Let's make this more concrete by looking at an example involving a dinosaur:

So we have this dinosaur, and our goal is to move this dinosaur 5 pixels with each call of requestAnimationFrame. On a 60Hz screen, this means our code will be called 60 times a second or once every 16.667 milliseconds. During this time, our dinosaur will have moved 300 pixels (5 pixels multiplied by 60):

On a 120Hz screen, because our requestAnimationFrame may get called 120 times a second, our dinosaur will have moved 600 pixels after one second (5 pixels multiplied by 120):

This isn't ideal, for we want our dinosaur to move a consistent distance after 1 second for all users across all varieties of devices.

This is where our delta time multiplier comes in. This multiplier acts on the thing whose value we are changing at each frame tick. In the case of our dinosaur, that thing is the 5 pixels we are moving the position by. On a 60Hz screen with a desired frame rate of 60 fps, we know the delta time multiplier is just 1. On a 120Hz screen with a desired frame rate of 60fps, we also know that the delta multiplier is a 0.5.

So, what we do is multiply 5 pixels (the thing that changes at each frame tick) by the delta time multiplier. On a 60Hz screen, our dinosaur will just move at 1 x 5 pixels at each frame tick. This results in 300 pixels of movement at the end of 1 second. On a 120Hz screen, our dinosaur will move at 0.5 x 5 pixels, or 2.5 pixels, at each frame tick. What is the end result? In this case, we multiply 120 by 2.5 pixels, which results in...300 pixels of movement. In both cases, our dinosaur will have now moved a total of 300 pixels at the end of 1 second even though the speed at which requestAnimationFrame fired at varied from 60 times a second to 120 times a second.

 

sd

 

sd

sd

Let's say that our goal is to move a ball 10 pixels every frame, which is every 16.667 milliseconds for a 60fps animation. Over one second, this means our ball will have moved 600 pixels. In a world where the delta time and frame time are consistent, this works perfectly. What if our raF is running at 120fps and our delta time is 8.333 milliseconds? This means our animation will run twice as fast where our ball will move 10 pixels every 8.333 milliseconds instead of every 16.667 milliseconds.

This is where the delta time multiplier comes in. We know that our animation is running twice as fast right now, so we can calculate the delta time by dividing our delta time by the frame time:

This would lead to a multiplier in this case of .5. Our animation code may be running twice as fast, but we are moving our ball by 10 * delta_multiplier which is 5 pixels. This ensures that even though our raF is firing 120 times a second, we are moving only half the distance at each frame, so the end result is that our ball still moves 600 pixels (120 frames by 5 pixels, the delta time multiplier).

Our delta time and delta time multiplier are calculated at each frame, so its values will be constantly revising based on the current browser and device situation:

The end result is an animation whose speed still remains fairly consistent, which is exactly what we want.

 

Code Explained

In the previous section, we looked at the code we need to add to make our animation code run at a fixed speed independent of how fast requestAnimationFrame runs. In this section, we’ll take a step back and really understand what is going on with our code and our thought process behind why we made some of the choices we did.

Key to all of this is the frame rate we are targeting to fix our animation to. Our code currently has frames_per_second set to 60. This means we want our animation code to be called 60 times in a second or...once every 16.667 milliseconds as calculated by 1000 milliseconds (aka 1 second) divided by 60:

By focusing on the elapsed time (sometimes referred to as the delta time) before running our code, we are no longer tied to the speed at which requestAnimationFrame runs. Our requestAnimationFrame can run 60 times a second, 75 times a second, 120 times a second, or at any other rate. It doesn’t matter. The only thing matters for us is whether 16.667 milliseconds have passed since the last time we called our animation code to ensure we hit our targeted goal of 60 frames a second.

If we adjust our target frame rate to be something other than 60, the same approach holds. Let’s say we set our frames_per_second value to 24 to have our animation code run 24 times a second. If we did this, the time between each call of our animation code would be 1/24 or 41.667 milliseconds:

Over a period of 1 second, when we call our animation code every 41.667 milliseconds, we would have called our code around 24 times. That is exactly what we want.

At this point, what role does requestAnimationFrame play in all of this. Our friendly requestAnimationFrame plays the hugely important role as the loop that runs to help us figure out how many milliseconds have elapsed between each call to our animation code. With each call of requestAnimationFrame, we check to see how much time has elapsed since the previous requestAnimationFrame call:

function animationLoop(timestamp) {
  currentTime = timestamp;
  elapsed = currentTime - previousTime;

  if (elapsed > interval) {
    previousTime = currentTime - (elapsed % interval);

    // add your animation code
  }

  requestAnimationFrame(animationLoop);
}

If enough time has passed (for example, 16.667 milliseconds for our target rate of 60fps), we call our animation code:

function animationLoop(timestamp) {
  currentTime = timestamp;
  elapsed = currentTime - previousTime;

  if (elapsed > interval) {
    previousTime = currentTime - (elapsed % interval);

    // add your visual updates-related code
  }

  requestAnimationFrame(animationLoop);
}

If enough time has not passed, we just let requestAnimationFrame run again and check on the next requestAnimationFrame call whether enough time has passed where we can call our animation code. This is what repeats forever and helps us create an animation whose frame rate is fixed. All of the surrounding code we see is designed to help us turn all of these explanations and diagrams into something our browser can understand.

Why not just use setInterval?

You may be asking yourself the following:

If all we need is a loop, why not just use setInterval as opposed to requestAnimationFrame where we can be guaranteed of a fixed frame rate and not worry about setInterval running faster or slower on some devices?

This is a totally valid question. For all of its quirks, requestAnimationFrame does represent the best approximation of when we can run some code that will make its way into a visual update. The browser may dynamically throttle requestAnimationFrame based on CPU or battery considerations, and having our animation code tied to a looping approach designed for animation-like activities is a good thing.

With setInterval, you are right in that our code is greatly simplified. Because setInterval is not in sync with our browser's repaint action, there is a high chance that setInterval will cause us to do animation-related work that is throwaway because it was done out of sync from when a repaint was about to happen. That is a worse problem than having to write a few extra lines of JavaScript as part of working with requestAnimationFrame.

Conclusion

An important part of animating with JavaScript is understanding the intricacies of requestAnimationFrame. To better help visualize the importance of frame rates and the role they play in ensuring a smooth animation, check out framerate.dev:

This mini-site allows you to see your current frame rate and what the animation will look like when you fix the frame rate to any of the choices provided. If you happen to check out the source code for how the frame rate setting works, you'll see it is nothing more than an adapted version of what we saw together in this article!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over 100,000 subscribers!

SUBSCRIBE NOW

Serving you freshly baked content since 1998!
Killer hosting by (mt) mediatemple

Twitter Youtube Facebook Pinterest Instagram Github