Request Animation Frame is magic…
and you shoud use it!
Optimizing animations, resize listeners and scroll events has given me a fever and the only cure is requestAnimationFrame. This isn’t a new API as it dates back to at least 2011, back when web developers would use timed loops to make changes every few milliseconds to the dom. At some point browser vendors thought “Hey maybe we should make a first class API for this and we can probably optimize it too!” They did just that and the API can be used for animation, DOM, canvas, throttling, or even WebGL.
First a little history!
Let's say we need to repeat something pretty fast but not too fast like a on scroll listener or a window resize event. Every time you resize the browser or scroll its going to call your listeners as fast as it can, which can be pretty bad when you are using method with performance impacts like getBoundingClientRect or animating something.
So we need a way to throttle calling our scroll or resize methods. The old school “cool kid” way would be something like this:
window.onscroll = function () {
didScroll = true;
console.log("on scroll");
};
setInterval(function () {
console.log("setInterval, 100");
if (didScroll) {
didScroll = false;
console.log("setInterval, do scroll stuff here");
}
}, 100);
Take a look at this codePen and checkout what is being logged.
https://codepen.io/flinehan/pen/MGyzQK
In this case we are now throttling the on scroll event to every 100ms! Nice, problem solved, we can all go home. Well not really… The issue with this is that setInterval will run constantly even if that tab or window is not visible! This is kind of a bummer in the day of battery powered devices. SetInterval also comes with a pretty big impact to the CPU, GPU, and memory usage since its always running. Oh and don’t even get me started about all the issues you are going to have if you want to have more than one element using this solution.
Why Should I use requestAnimationFrame?
The browser can optimize animations together into a single reflow and repaint cycle, which leads to some much smoother animations. Animations in inactive tabs will stop, allowing for less impact on the CPU, GPU, and memory of the page. So we could brag about having awesome animations that don’t drain batteries. It also only calls the method on every paint cycle, which is 60 fps.
How do I use this?
A super simple example would be:
function animateThings() {
// Do whatever
requestAnimationFrame(animateThings);
}
requestAnimationFrame(animateThings);
We recursively call our aniimateThings method so it animates as often as the browser will paint, which should be about 60 fps. If we had other animations triggered from our animateThings method, then the browser would optimize/batch them into one paint cycle, which is pretty awesome.
Here we have updated our old school scroll listener to use requestAnimationFrame and take a look at how often we fire our logs.
https://codepen.io/flinehan/pen/MGyzQK
Here is a really basic example of a animating div:
https://codepen.io/flinehan/pen/qYZLjQ
You can see we are moving that div as often as the browser will paint it. We are doing this by calling getBoundingClientRect and incrementing its location by a random amount. If you open your dev tools, hit esc, look at the console at the bottom of your page, click the tab called Rendering and check ‘Paint Flashing’. Now we can see what elements the browser animates by the green border it puts up. For some extra points check the FPS meter to see how our FPS is doing, we should be sitting at around a cool 60fps.
More than just Animations
Any time we have an event that fires multiple times per second we can use requestAnimationFrame to throttle it. This includes mouseOver, scroll, resize, and drag events. I am sure there are more, but those are the ones I have had the most experience using. Even if we aren’t animating in these events its worth while to throttle because we get all the optimizations that the browser brings.
Now some of these events are pretty global, I’m looking at you onScroll and resize events. We could create listener over and over again and then throttle them, but that isn’t very scalable. A better solution is to create a method that creates throttled events for us.
export const throttle = (type, name, obj) => {
obj = obj || window
let running = false
const func = () => {
if (running) {
return
}
running = true
requestAnimationFrame(() => {
obj.dispatchEvent(new CustomEvent(name))
running = false
})
}
obj.addEventListener(type, func)
}
Here we will use the browser provided CustomEvent Api to create a new event that gets throttled by the requestAnimationFrame api.
throttle("resize", "optimizedResize")
Calling the above code creates a listener for the resize event and then creates a new even called ‘optimizedResize’ which we can use throughout the application.
window.addEventListener("optimizedResize", this.onResize, false)
Boom in the above code we have a nice global solution for listening to one optimized resize event. Lets do the same thing for scroll events now too.
throttle("scroll", "optimizedScroll")
window.addEventListener("optimizedScroll", this.onScroll, false)
Resize Observer
Taking a look at the ResizeObserver polyfill we can start to understand what they are doing to make this work. Inside of their code they have methods that call getBoundingClientRect to check if the element has resized. Oh wait a second! Doesn’t that have an impact on triggering paints? Yes it does! But deep in their code you can see the secret sauce that gives them all their performance and you guessed it, they are using requestAnimationFrame.
https://github.com/pelotoncycle/resize-observer/blob/master/resize-observer.js#L160
Since the browser optimizes paints for us it isn’t that big of a deal to call getBoundingClientRect, because the browser will just bundle all those paints into its render cycle.
Go forth and requestAnimationFrame!