Performant CSS Animations

Web performance can be split into two relatively distinct categories: first page load and subsequent interactions. We can improve the first load by decreasing the server response time, and optimizing the loading of CSS and Javascript. Once the website is loaded, there is a completely new set of performance related challenges: using a Javascript framework with good inherent speed, immediately responding via the UI to input events, and building animations that feel snappy.

There are many posts online about all of these topics; however, the challenge is knowing what to look for. The goal of this post is to provide a basic level of understanding focusing on animation performance, explain why all animations are not created equal, and provide some other performance related resources.

A Simple jQuery Example

This is the canonical jQuery animation example. We take an element with a class selector and move it down 100px over 2 seconds. While jQuery makes it really easy to do this kind of animation, this actually has really bad performance implications. Let’s dig into what is actually happening. The above code is getting turned into something similar to this:

There are two major issues with this approach.

  • vSync
  • Animating Layout Properties

vSync

Notice the setTimeout in the example above. On a 60HZ monitor, we have 60 frames per second that we can update our animation. Our goal is to have a setTimeout callback fire each frame. There are many cases where this doesn’t actually happen. Among them, setTimeout and setInterval aren’t perfect and thus won’t fire exactly when we want it to, once per frame. Instead, you may end up with two updates on a single frame or a frame with no updates at all.

Thankfully, modern browsers provide a function window.requestAnimationFrame that takes a callback which the browser guarantees will be run on the next frame. You no longer need to try to calculate a setTimeout.

More Reading: http://www.html5rocks.com/en/tutorials/speed/rendering/

Animating Layout Properties

There is another reason why we might drop a frame. When an animation is happening, the browser executes your script on every frame, figures out what it needs to paint on the screen, and then paints it. Since this is likely happening about 60 times a second, the browser ends up with a little over 16ms to complete all of that. If a frame ever takes longer than the allotted time, the next frame is dropped so it can continue executing the previous frame.


From Google IO 2013

It becomes important to know what the browser is doing on every frame in order to optimize our animations and get rid of dropped frames. On every frame the browser has a set of steps it takes, which I’ll walk through below.

The Browser Frame Waterfall


From http://www.html5rocks.com/en/tutorials/speed/high-performance-animations/

Recalculate Style

The browser first runs your code and checks if any of the classes, styles, or attributes on elements have changed. If they have, it recalculates the styles for elements that might have changed, including inherited values.

Layout

The browser then calculates the size and position of every element on the screen, abiding by the rules of floats, absolute positioning, flexbox, etc.

Paint

The browser then draws all of the elements with their styles onto layers. Think of browser layers like Photoshop layers: collections of elements.

Composite

Once the browser has all of the elements drawn onto layers, it composites the layers onto the screen. This is just like rasterizing layers in Photoshop; it moves the layers around and draws them all into one image.


On every frame, the browser decides what has to be done. If a CSS rule changes the size of an element such as width, margin, or top, the browser will do a layout, paint, and composite.

If a CSS rule changes such as border-color, background-position, or z-index, the browser doesn’t need to recalculate the layout, but it needs to repaint and composite the layers together.

There are a few rules that don’t require a layout or a paint and the browser can do with just a composite. transform and opacity are the most common ones. Just like in Photoshop, moving layers around is very fast. Photoshop doesn’t need to think about what is on the layer; it just moves the entire layer. The browser can do the same thing. Even better, while calculating element sizes for a layout happens on the CPU, things that only require a composite can happen purely on the GPU which is much faster.

http://csstriggers.com is a great resource to learn more about which CSS properties cause a layout, paint, and composite.

More Reading: http://www.html5rocks.com/en/tutorials/speed/high-performance-animations/#toc-dom-to-pixels

What Can We Animate

The key to fast and smooth animations is to only animate things that require layer compositing which is done on the GPU. opacity, and transform: translate, scale, and rotate are our toolkit to work with.

Hover over the boxes to see the effect

See the Pen mywqNB by Eli White (@TheSavior) on CodePen.

See the Pen KwqyOJ by Eli White (@TheSavior) on CodePen.

See the Pen gbRoQQ by Eli White (@TheSavior) on CodePen.

See the Pen rawpoW by Eli White (@TheSavior) on CodePen.

Putting these examples together to do more complicated animations is an exercise in thinking outside the box. Here are some examples.

Animate background-color?

See the Pen YPQEbZ by Eli White (@TheSavior) on CodePen.

The effect is simple. We have two identical overlapping boxes in the same place, one red and one teal. The teal starts at opacity: 0 and on hover of the containing box we transition the teal to opacity: 1.

Alert message

See the Pen zxzPVY by Eli White (@TheSavior) on CodePen.

In order to have a simple message slide in on a div, we position: absolute the alert box and translate it -50px up. Since the parent div has overflow: hidden, we don’t see it. On hover of the parent div, we slide the alert box back down to 0.

Flipping Div

See the Pen zxzRvd by Eli White (@TheSavior) on CodePen.

While a little less useful, this is a good example of thinking outside (or behind) the box. While most people would think animations like this would be inefficient, knowing what the browser is doing and clever ways to put things together can actually open doors of possibilities. Check out the codepen for the CSS.

Revisiting the setTimeout Example

We now know the above code is bad!

The setTimeout example that we dissected at the start of this post can be easily modified to take advantage of the knowledge we know now.

As we pointed out at the beginning, the big issues are setting top which is a CSS property which causes a layout to happen and thus also a paint and composite. We will change that to a translate.

The other issue was that using setTimeout doesn’t guarantee our animation to be run on every frame. We will change that to requestAnimationFrame. requestAnimationFrame also gives us as a parameter the time since the app started. By changing around our math a little bit to be based on time instead of distance for step and current, our animation will look smooth even if we still miss a frame.

We end up with our animation performance optimized code.

Conclusion

There are three key takeaways to building performant web animations.

  • Know how the browser executes your animations
  • Prefer to animate things that only require compositing while avoiding causing a layout
  • Use requestAnimationFrame over setTimeout or setInterval

In my experience, the hardest part of building a site that has well written animations is actually in the knowledge and communication between designers and engineers. Designers need to understand what palette and toolkit we have to work with to be able to come up with interactions and metaphors that can feel smooth to the user. Engineers need to know how to implement specific ideas and debug what the browser is doing when running their animations.

At the end of the day, all that matters is the experience the client has with the application. If we build things that are slow and jittery, that satisfaction will go down. It is on all of us to continue improving, continue educating ourselves, and continue building awesome experiences.

More Reading