Understanding animation, duration and easing using requestAnimationFrame

Creating performant animations with requestAnimationFrame

Sam Pakvis
Burst
7 min readMar 8, 2020

--

It’s the greatest invention since the stroopwafel: requestAnimationFrame. It’s the cornerstone of your favourite animation library, and it’s a great way to create your own animations. In canvas, for example. But how does it work? Weirdly enough, I see a lot of front-end developers run faster from requestAnimationFrame documentation than they do from someone coughing on the train these days. And I get it. It’s scary. It’s about functions calling themselves in some sort of loop, timestamps, performance? Let’s find out.

What is animation?

In order to understand how to work with requestAnimationFrame, we’re going to have to understand motion first. The human brain can’t really see objects move. When we look at a bouncing ball, the average human eye behaves like a photo camera and takes about one hundred and fifty (150) little snapshots per second and sends those images to the brain. The brain perceives this as motion, because every snapshot is a little further in time and thus the movement has progressed a little more. In animation, these snapshots are called frames.

Creating frames for animation

Let’s say we want to animate a red square from left to right. In order to do so, we’re going to need to create those frames we’ve talked about. Because we need to move the red square in each frame a little more to the right in order to create motion, we’re going to have to execute a function for each frame.

“One hundred and fifty frames per second? Excuse me?” — Haha yeah, I know. That’s a lot of function executions per second. Fortunately, we don’t need to perfectly mimic the human eye to achieve smooth animation. Opinions differ around this topic, though. Some say 24 frames per second (fps) is enough. I say: “screw that”. We should always aim for 60 fps.

Thirty (30) frames per second
Sixty (60) frames per second

“Sixty frames per second? Excuse me?” — Still probably you right now. But don’t worry, my performance hunting friend.

Getting optimal performance with requestAnimationFrame

In order to get to those optimal 60 frames per second (fps), we could use a setInterval and run that function sixty times per second. The problem is that if the user’s computer is too busy running your useless WebGL website, it doesn’t have enough resources to execute those sixty function calls per second but it will still try to do so.

Understanding why this isn’t going to work and what’s happening behind the scenes of the browser in the event loop is beyond the scope of this article, but just know: your computer slows down, animations don’t get executed the way they should and everything just goes to shit.

In 2018, Jake Archibald gave an amazing talk on the event loop. It’s a must-watch if you’re interested in how requestAnimationFrame works behind the scenes

Thankfully, the mighty performance gods have given us requestAnimationFrame, a method available on the window object. When you call it, it’ll execute a callback function whenever it has enough resources to do a repaint of everything that’s on your current screen. You’re telling the browser: “Please queue this task, and execute when you’re ready to repaint”.

Example of a single requestionAnimationFrame

But this only gets executed once. When we animate something, we want to get those 60 executions per second, to achieve animations with a decent amount of frames per second. But how do we do this?

First of all, we need to create some sort of loop to continuously request more frames.

https://codepen.io/trekinbami/pen/MWwPgWB — We’re telling the browser to keep requesting new frames in which we move the red square 5 pixels if we haven’t reached the end goal of 300 pixels

And here is where we first harness the power of requestAnimationFrame. In the animate function, we’re essentially creating a new frame by giving the square a new position. But in order to get smooth animation, this should happen sixty times per second. We do this by calling the animate function from itself with the result that it keeps repeating itself. When the browser has the resources, it will render another frame with what’s happening in the animate function. And because we’re calling the animate function from itself, it will go in an infinite loop until we tell it to stop.

But how does requestAnimationFrame get to 60(?) FPS?

Because you assign your animate function as a callback to your requestAnimationFrame it only gets called when requestAnimationFrame sees an opportunity to repaint the browser window. Per the spec, it will try to do so for a maximum amount that is synced with the refresh rate of your monitor (Hz). Most monitors are 60Hz, that’s why requestAnimationFrame will try to run sixty times per second (60fps). With a 120Hz monitor it’ll try to run 120 times per second (120fps).

I emphasised maximum there, because it’s not going to render more frames per second than the monitor is capable of showing.

But like we said before, requestAnimationFrame only fires when there are enough resources to do a repaint. What if you’re running those custom written GLSL shaders in your code, that makes your computer hotter than Henry Cavill with a moustache? That would mean you’re low on CPU resources and it might be possible that requestAnimationFrame executes your callback only 30 times per second.

Apparently there’s no way to know how many frames requestAnimationFrame is going to give us. But if one user has 30 frames and the other has 120 frames per second, and you would animate five pixels per frame, it would mean that every user would see a different animation. Well, the smart specification people thought about that as well.

Timing your animations with requestAnimationFrame

Since the amount of frames is arbitrary, let’s forget about them for now and work with what we do know: time.

The requestAnimationFrame passes one argument to your callback. It’s a timestamp with how much milliseconds have passed since the document loaded. Based on that timestamp we’re going to calculate what our new left (translateX) value should be based on the time that has passed, and let requestAnimationFrame worry about the frames. You can read that again if you want. Slowly. Out loud. It’s complex. But you can handle it.

1.1 When you hypothetically only have four frames because your CPU is too busy with Slack and Chrome

Let’s write this up in code. Don’t worry, we’ll go through it step by step. First up: calculate the relative time progression, also known as normalisation of the value:

I don’t want to repeat the comments in the code, so please read them carefully before continuing

Now that we have our relative progression (a float between 0.0 and 1.0) we can calculate what our new translateX value should be:

https://codepen.io/trekinbami/pen/NWqOKxX

In each frame we’re calculating the new left position based on time passed since our animation started. In this way it doesn’t matter how many frames the user gets. More frames per second means less pixels to animate per frame. Less frames per second means more pixels to animate per frame.

Now to the last part of this article. I promise. Really.

How to use easing in requestAnimationFrame

Easing is like when the animation first goes fast, then a little slower and then fast again right? How does that work? Well, it’s math. And I’m not a math teacher. In fact, I absolutely suck at math. But maybe we can simplify things a bit.

We talked about relative progression earlier. We were calculating a value between 0.0 and 1.0 based on the time passed. If we were to plot that into a graph, it would be pretty linear:

Linear progression

But what happens if we would cheat our animation? That we would tell it 0.4 seconds have passed, when actually it was 0.2 seconds. And after that tell it 0.6 seconds have passed, when it was actually 0.8 seconds that had passed. The graph would like this:

Non-linear progression

This is what easing functions do. They tell the animation to go to a certain point in time that’s off the linear path.

If we want to implement that in our example it wouldn’t be much of a hassle, because we’ve got our relative progression already. I’m going to use the easeInSine easing. We can use it in our javascript by using the Bezier Easing NPM package:

https://codesandbox.io/s/friendly-sunset-6sgly

Yeah, I know. This is the shitty life of a front-end developer. Writing 30 lines of code to move a red square a couple of pixels to the right. But congratulations! You made it ’till the end! Thank you for reading, and hopefully you became a little bit smarter.

If you have any questions, don’t hesitate to leave a comment or DM me at https://twitter.com/trekinbami.

XOXO.

--

--