Understanding JavaScript's requestAnimationFrame() method for smooth animations
Updated: Nov 1st, 2017
The modern web of today is filled with sights to behold on every page,
where menus slide in and out, content gently fade into view, and elements
animate around the screen as the user scrolls the page. While CSS3 has
supplanted JavaScript in many cases to help power these rich animations in an
intuitive, well optimized manner, JavaScript will always have a role to play in
more complex scenarios that involve user interaction or non linear logic. It is
because of this that the requestAnimationFrame()
method was introduced, to help
us execute animation related JavaScript code that make changes to the user's
screen in an efficient, optimized manner. You've no doubt heard about this
method before, though like many people may not quite understand its benefits or
how to properly use it yet. In this tutorial we'll get you all caught up so you
can finally start taking advantage of requestAnimationFrame()
to
perform animations more optimally in your scripts. Let's roll!
Why we need
another hero- requestAnimationFrame
First of all, lets talk about requestAnimationFrame()
as an idea
and why we even need such a method. Traditionally to create an animation in
JavaScript, we relied on setTimeout()
called recursively or setInterval()
to
repeatedly execute some code to make changes to an element frame by frame, such
as once every 50 milliseconds:
var adiv = document.getElementById('mydiv') var leftpos = 0 setInterval(function(){ leftpos += 5 adiv.style.left = leftpos + 'px' // move div by 5 pixels each time }, 50) // run code every 50 milliseconds
While the above code is logically sound, its actual execution is far from perfect. The problem with using setTmeout/setInterval for executing code that changes something on the screen is twofold.
-
What we specify as the delay (ie: 50 milliseconds) inside these functions are often times not honoured due to changes in user system resources at the time, leading to inconsistent delay intervals between animation frames.
-
Even worse, using
setTimeout()
orsetInterval()
to continuously make changes to the user's screen often induces "layout thrashing", the browser version of cardiac arrest where it is forced to perform unnecessary reflows of the page before the user's screen is physically able to display the changes. This is bad -very bad- due to the taxing nature of page reflows, especially on mobile devices where the problem is most apparent, with janky page loads and battery drains. An iPhone or two have even caught fire as a result (just a joke Apple, no law suits please)!
requestAnimationFrame()
to the rescue
It is for the above reasons requestAnimationFrame()
was
introduced. The method in a nutshell allows you to execute code on the next
available screen repaint, taking the guess work out of getting in sync with the
user's browser and hardware readiness to make changes to the screen. When we
call requestAnimationFrame()
repeatedly to create an animation, we are assured
that our animation code is called when the user's computer is actually
ready to make changes to the screen each time, resulting in a smoother, more
efficient animation. Furthermore, code called via requestAnimationFrame()
and
running inside background tabs in your browser are either paused or slowed down
significantly (to 2 frames per second or less) automatically to further save
user system resources- there's no point in running an animation that isn't being
seen is there?
So requestAnimationFrame()
should be used in place of setTimeout/
setInterval for animations, but how exactly do we go about doing that? Before we
get to that, lets look at browser support first. requestAnimationFrame()
today
enjoys
wide
adoption amongst modern browsers- IE10+, FF11+, Chrome, and Safari etc. For
some older versions of these browsers, a vendor prefix is needed in front of the
method name to work. Even on browsers that don't support this method in any
incarnation, we can simply fallback in those cases to setTimeout()
instead to
call the code after a certain delay instead. The following code creates a
universal rrequestAnimationFrame()
and its counterpart cancelAnimationFrame()
function that works in the maximum number of browsers, with a fallback built in:
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function(f){return setTimeout(f, 1000/60)} // simulate calling code 60 window.cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame || function(requestID){clearTimeout(requestID)} //fall back
For the fallback setTimeout()
code, notice the delay being set
at 1000/60, or around 16.7 milliseconds. This value simulates how often the real
requestAnimationFrame()
will typically be called by the browser each time it's invoked based on the
typical user's screen refresh rate of 60 frames per second.
Understanding
and using requestAnimationFrame()
The syntax for requestAnimationFrame
is very straightforward:
requestAnimationFrame(callback)
We enter a callback function containing the code we wish to run,
and requestAnimationFrame()
will run it when the screen is ready to accept the
next screen repaint. Some noteworthy details:
-
The callback function is automatically passed a timestamp indicating the precise time
requestAnimationFrame()
was called. -
requestAnimationFrame()
returns a non 0 integer that can be passed into its nemesis counterpartcancelAnimationFrame()
to cancel arequestAnimationFrame()
call
Here's an example of calling requestAnimationFrame()
once to
move a DIV just 5 pixels from its original location on the screen:
var adiv = document.getElementById('mydiv') var leftpos = 0 requestAnimationFrame(function(timestamp){ leftpos += 5 adiv.style.left = leftpos + 'px' })
The above code is very similar to using setTimeout()
to run the
same code, except instead of after the user defined delay, the code is called on
the next available screen repaint, typically around 16.7 milliseconds based on a
typical screen refresh rate of 60fps. The exact number may fluctuate and frankly
doesn't matter; what's important to realize is that the browser will now
intelligently invoke our code only when it is ready to accept changes to the
screen, not before.
Calling requestAnimationFrame()
once is pretty meaningless most
of the time. The magic happens when we call it "recursively" to construct
the desired animation frame by frame, with each frame being called only when the
browser is ready for it. This this how requestAnimationFrame()
becomes superior
to setTimeout
or setInterval
when it comes to handling animation related code
efficiently. Lets rewrite our initial example of moving a DIV across the screen
5 pixels at a time using requestAnimationFrame()
:
var adiv = document.getElementById('mydiv') var leftpos = 0 function movediv(timestamp){ leftpos += 5 adiv.style.left = leftpos + 'px' requestAnimationFrame(movediv) // call requestAnimationFrame again to animate next frame } requestAnimationFrame(movediv) // call requestAnimationFrame and pass into it animation function
The above code shows the basic blueprint for using requestAnimationFrame()
to create an animation, by defining your animation code inside a function, then
inside this function calling itself recursively through requestAnimationFrame()
to produce each frame of our animation. To kick start the animation, we make a
call to requestAnimationFrame()
outside the animation function with that
function as the parameter.
Animation
over time in requestAnimationFrame()
So it's simple enough to repeatedly call an animation function
using requestAnimationFrame()
, but most animations are much more
finicky,
having to stop at some point after a certain objective has been achieved over a
certain amount of time. Take our example of moving the DIV above; in a real life
scenario, what we probably want to do is move the DIV 400 pixels to the right
over a time of say 2 seconds. To do this with requestAnimationFrame()
, we can
take advantage of the timestamp
parameter that's passed into the callback
function. Lets see how this works now, by retooling our DIV moving code above so
it moves the DIV a certain distance over a certain amount of time:
var adiv = document.getElementById('mydiv') var starttime function moveit(timestamp, el, dist, duration){ //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date: var timestamp = timestamp || new Date().getTime() var runtime = timestamp - starttime var progress = runtime / duration progress = Math.min(progress, 1) el.style.left = (dist * progress).toFixed(2) + 'px' if (runtime < duration){ // if duration not met yet requestAnimationFrame(function(timestamp){ // call requestAnimationFrame again with parameters moveit(timestamp, el, dist, duration) }) } } requestAnimationFrame(function(timestamp){ starttime = timestamp || new Date().getTime() //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date moveit(timestamp, adiv, 400, 2000) // 400px over 1 second })
Demo:
Lets go over how this works now.
-
Just before the animation runs, we set the startime variable to the current time using either
requestAnimationFrame
's timestamp parameter, or ifrequestAnimationFrame
isn't supported, a less precisenew Date().getTime()
instead. The former is a value automatically passed in as the first parameter of the callback function ofrequestAnimationFrame
that contains a highly accurate representation of the current time in milliseconds (accurate to 5 microseconds). This lets us know when the animation started running. -
Inside the animation function
moveit()
, we capture the current time of the current "frame" using variabletimestamp
. We use the difference between that and the animationstarttime
to figure out at what "point" along the animation we're currently at, and change the DIV's position accordingly out of the total distance (ie: 400px).
Slowing down
or cancelling requestAnimationFrame()
The standard requestAnimationFrame
runs at around 60fps under
ideal conditions (or once every 16.7ms), in sync with the refresh rate of the
typical monitor. If your animation requires a different frames per second (up to
60 fps) or simply doesn't require that high a level of refresh rate, you can slow it
down by calling requestAnimationFrame
inside setTimeout()
. That way, you get the
desired frame rate while reaping the benefits of requestAnimationFrame
:
var adiv = document.getElementById('mydiv') var leftpos = 0 var fps = 20 function movediv(timestamp){ setTimeout(function(){ //throttle requestAnimationFrame to 20fps leftpos += 5 adiv.style.left = leftpos + 'px' requestAnimationFrame(movediv) }, 1000/fps) } requestAnimationFrame(movediv)
In this version of moving a DIV horizontally, we're
throttling the frames per second to roughly 20, by calling requestAnimationFrame
inside setTimeout()
each time.
- Cancelling requestAnimationFrame()
Just like with setTimeout
/ setInterval
, you can cancel a
requestAnimationFrame
call, and in identical fashion as well.
requestAnimationFrame
when called returns a non 0 integer that can be captured
inside a variable and passed into its nemesis counterpart cancelAnimationFrame()
to stop it from being invoked again. The following logs the timestamp
parameter
value of requestAnimationFrame
for two seconds, using cancelAnimationFrame
to stop the former:
var reqanimationreference function logtimestamp(timestamp){ console.log(timestamp) reqanimationreference = requestAnimationFrame(logtimestamp) } requestAnimationFrame(logtimestamp) setTimeout(function(){ // cancel requestAnimationFrame after 2 seconds cancelAnimationFrame(reqanimationreference) }, 2000)
Here is a slightly more elaborate example that continuously
changes a DIV's width using requestAnimationFrame
when the mouse enters
the parent container (onmouseenter
), and cancels it onmouseleave
:
Demo:
Conclusion
As you can see, requestAnimationFrame()
is actually very simple
in concept and execution, once you understand its purpose and common
patterns. A lot of frameworks such as
jQuery 3.0 utilize requestAnimationFrame()
internally for all
animation related functions, though it's definitely better to understand how
this method works natively so you can take advantage of it in any JavaScript
environment.
End of Tutorial