Animating like you just don’t care with Element.animate

In Firefox 48 we’re shipping the <a href="https://developer.mozilla.org/docs/Web/API/Element/animate" target="_blank"><b>Element.animate()</b></a> API — a new way to programmatically animate DOM elements using JavaScript. Let’s pause for a second — “big deal”, you might say, or “what’s all the fuss about?” After all, there are already plenty of animation libraries to choose from. In this post I want to explain what makes Element.animate() special.

What a performance

Element.animate() is the first part of the Web Animations API that we’re shipping and, while there are plenty of nice features in the API as a whole, such as better synchronization of animations, combining and morphing animations, extending CSS animations, etc., the biggest benefit of Element.animate() is performance. In some cases, Element.animate() lets you create jank-free animations that are simply impossible to achieve with JavaScript alone.

Don’t believe me? Have a look at the following demo, which compares best-in-class JavaScript animation on the left, with Element.animate() on the right, whilst periodically running some time-consuming JavaScript to simulate the performance when the browser is busy.

Performance of regular JavaScript animation vs Element.animate()To see for yourself, try loading the demo in the latest release of Firefox or Chrome. Then, you can check out the full collection of demos we’ve been building!

When it comes to animation performance, there is a lot of conflicting information being passed around. For example, you might have heard amazing (and untrue) claims like, “CSS animations run on the GPU”, and nodded along thinking, “Hmm, not sure what that means but it sounds fast.” So, to understand what makes Element.animate() fast and how to make the most of it, let’s look into what makes animations slow to begin with.

Animations are like onions (Or cakes. Or parfait.)

In order for an animation to appear smooth, we want all the updates needed for each frame of an animation to happen within about 16 milliseconds. That’s because browsers try to update the screen at the same rate as the refresh rate of the display they’re drawing to, which is usually 60Hz.

On each frame, there are typically two things a browser does that take time: calculating the layout of elements on the page, and drawing those elements. By now, hopefully you’ve heard the advice, “Don’t animate properties that update layout.” I am hopeful here — current usage metrics suggest that web developers are wisely choosing to animate properties like transform and opacity that don’t affect layout whenever they can. (color is another example of a property that doesn’t require recalculating layout, but we’ll see in a moment why opacity is better still.)

If we can avoid performing layout calculations on each animation frame, that just leaves drawing the elements. It turns out that programming is not the only job where laziness is a virtue — indeed animators worked out a long time ago that they could avoid drawing a bunch of very similar frames by creating partially transparent cels, moving the cels around on top of the background, and snapshotting the result along the way.

Example of cels used to create animation frames

Example of creating animation frames using cels.
(Of course, not everyone uses fancy cels; some people just cut out Christmas cards.)

A few years ago browsers caught on to this “pull cel” trick. Nowadays, if a browser sees that an element is moving around without affecting layout, it will draw two separate layers: the background and the moving element. On each animation frame, it then just needs to re-position these layers and snapshot the result without having to redraw anything. That snapshotting (more technically referred to as compositing) turns out to be something that GPUs are very good at. What’s more, when they composite, GPUs can apply 3D transforms and opacity fades all without requiring the browser to redraw anything. As a result, if you’re animating the transform or opacity of an element, the browser can leave most of the work to the GPU and stands a much better chance of making its 16ms deadline.

Hint: If you’re familiar with tools like Firefox’s Paint Flashing Tool or Chrome’s Paint Rectangles you’ll notice when layers are being used because you’ll see that even though the element is animating nothing is being painted! To see the actual layers, you can set layers.draw-borders to true in Firefox’s about:config page, or choose “Show layer borders” in Chrome’s rendering tab.

You get a layer, and you get a layer, everyone gets a layer!

The message is clear — layers are great and you are expecting that surely the browser is going to take full advantage of this amazing invention and arrange your page’s contents like a mille crêpe cake. Unfortunately, layers aren’t free. For a start, they take up a lot more memory since the browser has to remember (and draw) all the parts of the page that would otherwise be overlapped by other elements. Furthermore, if there are too many layers, the browser will spend more time drawing, arranging, and snapshotting them all, and eventually your animation will actually get slower! As a result, a browser only creates layers when it’s pretty sure they’re needed — e.g. when an element’s transform or opacity property is being animated.

Sometimes, however, browsers don’t know a layer is needed until it’s too late. For example, if you animate an element’s transform property, up until the moment when you apply the animation, the browser has no premonition that it should create a layer. When you suddenly apply the animation, the browser has a mild panic as it now needs to turn one layer into two, redrawing them both. This takes time, which ultimately interrupts the start of the animation. The polite thing to do (and the best way to ensure your animations start smoothly and on time) is to give the browser some advance notice by setting the will-change property on the element you plan to animate.

For example, suppose you have a button that toggles a drop-down menu when clicked, as shown below.

Example of using will-change to prepare a drop-down menu for animation

Live example

We could hint to the browser that it should prepare a layer for the menu as follows:

nav {
  transition: transform 0.1s;
  transform-origin: 0% 0%;
  will-change: transform;
}
nav[aria-hidden=true] {
  transform: scaleY(0);
}

But you shouldn’t get too carried away. Like the boy who cried wolf, if you decide to will-change all the things, after a while the browser will start to ignore you. You’re better off to only apply will-change to bigger elements that take longer to redraw, and only as needed. The Web Console is your friend here, telling you when you’ve blown your will-change budget, as shown below.

Screenshot of the DevTools console showing a will-change over-budget warning.

Animating like you just don’t care

Now that you know all about layers, we can finally get to the part where Element.animate() shines. Putting the pieces together:

  • By animating the right properties, we can avoid redoing layout on each frame.
  • If we animate the opacity or transform properties, through the magic of layers we can often avoid redrawing them too.
  • We can use will-change to let the browser know to get the layers ready in advance.

But there’s a problem. It doesn’t matter how fast we prepare each animation frame if the part of the browser that’s in control is busy tending to other jobs like responding to events or running complicated scripts. We could finish up our animation frame in 5 milliseconds but it won’t matter if the browser then spends 50 milliseconds doing garbage collection. Instead of seeing silky smooth performance our animations will stutter along, destroying the illusion of motion and causing users’ blood pressure to rise.

However, if we have an animation that we know doesn’t change layout and perhaps doesn’t even need redrawing, it should be possible to let someone else take care of adjusting those layers on each frame. As it turns out, browsers already have a process designed precisely for that job — a separate thread or process known as the compositor that specializes in arranging and combining layers. All we need is a way to tell the compositor the whole story of the animation and let it get to work, leaving the main thread — that is, the part of the browser that’s doing everything else to run your app — to forget about animations and get on with life.

This can be achieved by using none other than the long-awaited Element.animate() API! Something like the following code is all you need to create a smooth animation that can run on the compositor:

elem.animate({ transform: [ 'rotate(0deg)', 'rotate(360deg)' ] },
             { duration: 1000, iterations: Infinity });

Screenshot of the animation produced: a rotating foxkeh
Live example

By being upfront about what you’re trying to do, the main thread will thank you by dealing with all your other scripts and event handlers in short order.

Of course, you can get the same effect by using CSS Animations and CSS Transitions — in fact, in browsers that support Web Animations, the same engine is also used to drive CSS Animations and Transitions — but for some applications, script is a better fit.

Am I doing it right?

You’ve probably noticed that there are a few conditions you need to satisfy to achieve jank-free animations: you need to animate transform or opacity (at least for now), you need a layer, and you need to declare your animation up front. So how do you know if you’re doing it right?

The animation inspector in Firefox’s DevTools will give you a handy little lightning bolt indicator for animations running on the compositor. Furthermore, as of Firefox 49, the animation inspector can often tell you why your animation didn’t make the cut.

Screenshot showing DevTools Animation inspector reporting why the transform property could not be animated on the compositor.

See the relevant MDN article for more details about how this tool works.

(Note that the result is not always correct — there’s a known bug where animations with a delay sometimes tell you that they’re not running on the compositor when, in fact, they are. If you suspect DevTools is lying to you, you can always include some long-running JavaScript in the page like in the first example in this post. If the animation continues on its merry way you know you’re doing it right — and, as a bonus, this technique will work in any browser.)

Even if your animation doesn’t qualify for running on the compositor, there are still performance advantages to using Element.animate(). For instance, you can avoid reparsing CSS properties on each frame, and allow the browser to apply other little tricks like ignoring animations that are currently offscreen, thereby prolonging battery life. Furthermore, you’ll be on board for whatever other performance tricks browsers concoct in the future (and there are many more of those coming)!

Conclusion

With the release of Firefox 48, Element.animate() is implemented in release versions of both Firefox and Chrome. Furthermore, there’s a polyfill (you’ll want the web-animations.min.js version) that will fall back to using requestAnimationFrame for browsers that don’t yet support Element.animate(). In fact, if you’re using a framework like Polymer, you might already be using it!

There’s a lot more to look forward to from the Web Animations API, but we hope you enjoy this first installment (demos and all)!

About Brian Birtles

Brian works on animations and layout in Firefox at Mozilla in Tokyo, Japan. He also edits the W3C Web Animations and CSS Animations specifications and has been trying to animate SVG for far too long. Despite his profile picture he is a big fan of the ocean and dreams of swimming or surfing to work.

More articles by Brian Birtles…


9 comments

  1. Simon T

    Aren’t your demos misleading as they purposely add random delays?

    August 3rd, 2016 at 10:50

    1. Brian Birtles

      Hi Simon!
      The purpose of the initial demo is to simulate a browser under load, i.e. a situation where you will experience jank. The spurts of long-running JS produce the same effect as if the main thread were tied up running event handlers, performing GC, or any other long-running operation. Since the focus of the article is on eliminating animation jank I think that’s a reasonable demo but perhaps you’re suggesting I could have made that more clear?

      August 3rd, 2016 at 16:11

      1. Simon T

        Hi Brian,

        Yeah, I felt like the one demo was a bit of an extreme example without a bit of a disclaimer. When running the demo I was surprised that it was actually stuttering which is why I took a peek at the source & commented.

        The full collection of demos was quite good though and I was easily able to see differences in performance between JS and CSS without the need of artificial delays.

        Simon

        August 3rd, 2016 at 16:36

        1. Brian Birtles

          Fair point! I’ve updated the introduction to that demo to point out what it’s doing. And I’m glad the other demos were helpful! Thanks Simon!

          August 3rd, 2016 at 16:40

  2. Christoph O

    Thanks for keeping us up-to-date with the current state of the Animation API. I’m really excited about this feature and happy to see that some parts are already being supported by current browsers.

    I was curious about the performance of the new API on Android comparing Firefox and Chrome in their latest stable versions, as well as on a Windows machine. To my surprise all of the demos (https://mozdevs.github.io/Animation-examples/) are running so much smoother in Chrome than in Firefox (Android and Windows) and even look nicer due to Anti-Aliasing. (See a recording of the both versions running next to each other: https://youtu.be/qB6YkClUAUw) The machine was throttled to 1.2GHz (8GB Ram, Core i7).

    What could be the problem here? Even on Android, there is a huge difference between Firefox and Chrome. I’ve also tried the nightly versions of Firefox, but no improvements there.

    August 6th, 2016 at 03:49

    1. Brian Birtles

      Hi Cristoph,

      Thanks for your comment. In the video you recorded the animation is running on the main thread because it uses transform-style: preserve-3d which Gecko currently doesn’t do on the compositor. Mozilla bug 779598 and bug 1208646 track this work which should fix the performance considerably.

      As a side note, you can see this yourself by opening the animations inspector on that page and looking at the animations. If you expand any of the animations you’ll see that ‘transform’ has a dotted underline and when you hover over it, it should let you know that it is not running on the compositor due to using transform-style: preserve-3d.

      August 8th, 2016 at 16:28

      1. Christoph O

        Hi Brian,

        Thanks for the clarification. I was afraid this demo wasn’t utilizing the GPU at all, but didn’t know why. Keep up with the good work!

        Regards,
        Christoph

        August 10th, 2016 at 00:31

  3. Claire

    Hi Brian,
    I have been learning to become a graphics designer. Your tutorial is very helpful and interesting. I am happy that i come across your article. After checking this post , i am more motivated to improve my skills as a designer.
    Regards,
    Claire

    August 8th, 2016 at 02:25

    1. Brian Birtles

      Thanks Claire! I’m glad to hear you’re motivated to continue improving your design skills!

      August 8th, 2016 at 16:29

Comments are closed for this article.