We’re going to cut straight to the chase. Modern browsers can animate four things really cheaply: position, scale, rotation and opacity. If you animate anything else, it’s at your own risk, and the chances are you’re not going to hit a silky smooth 60fps.
Take a look at this side-by-side slow motion video of the same animation:
One is done with transforms, the other isn’t. You can see what a difference it makes, so let’s take look at why that is.
From DOM to Pixels in DevTools
When you use the Chrome DevTools timeline you should see a pattern similar to this:
The process that the browser goes through is pretty simple: calculate the styles that apply to the elements (Recalculate Style), generate the geometry and position for each element (Layout), fill out the pixels for each element into layers (Paint Setup and Paint) and draw the layers out to screen (Composite Layers).
To achieve silky smooth animations you need to avoid work, and the best way to do that is to only change properties that affect compositing -- transform and opacity. The higher up you start on the timeline waterfall the more work the browser has to do to get pixels on to the screen.
This advice is broadly cross-browser compatible. Chrome, Firefox, Safari and Opera all hardware accelerate transforms and opacity. Unfortunately it is not clear what criteria Internet Explorer 10+ uses to determine if hardware acceleration is suitable, but hopefully that will become clearer when the F12 tools in IE11 are shipped.
Animating Layout Properties
When you change elements, the browser may need to do a layout, which involves calculating the geometry (position and size) of all the elements affected by the change. If you change one element, the geometry of other elements may need to be recalculated. For example, if you change the width of the
<html> element any of its children may be affected. Due to the way elements overflow and affect one another, changes further down the tree can sometimes result in layout calculations all the way back up to the top.
The larger the tree of visible elements, the longer it takes to perform layout calculations, so you must take pains to avoid animating properties that trigger layout.
Are you storing app state in the or elements with classes? When these elements are changed, the browser may have to redo style calculations and layout. Watch out for inadvertent layout triggers in your apps; they may not animate but they can be very expensive!
Here are the most popular CSS properties that, when changed, trigger layout:
Styles that affect layout
Animating Paint Properties
Changing an element may also trigger painting, and the majority of painting in modern browsers is done in software rasterizers. Depending on how the elements in your app are grouped into layers, other elements besides the one that changed may also need to be painted.
If you’re new to the concept of layers you should read Tom Wiltzius’s introduction to the topic.
There are many properties that will trigger a paint, but here are the most popular:
Styles that affect paint
If you animate any of the above properties the element(s) affected are repainted, and the layers they belong to are uploaded to the GPU. On mobile devices this is particularly expensive because CPUs are significantly less powerful than their desktop counterparts, meaning that the painting work takes longer; and the bandwidth between the CPU and GPU is limited, so texture uploads take a long time.
Animating Composite Properties
There is one CSS property, however, that you might expect to cause paints that sometimes does not: opacity. Changes to opacity can be handled by the GPU during compositing by simply painting the element texture with a lower alpha value. For that to work, however, the element must be the only one in the layer. If it has been grouped with other elements then changing the opacity at the GPU would (incorrectly) fade them too.
In Blink and WebKit browsers a new layer is created for any element which has a CSS transition or animation on opacity, but many developers use
translate3d(0,0,0) to manually force layer creation. Forcing layers to be created ensures both that the layer is painted and ready-to-go as soon as the animation starts (creating and painting a layer is a non-trivial operation and can delay the start of your animation), and that there's no sudden change in appearance due to antialiasing changes. Promoting layers should done sparingly, though; you can overdo it and having too many layers can cause jank.
In Chrome, non-root, non-opaque layers use grayscale antialiasing rather than subpixel antialiasing, which can be very noticeable, especially if the antialiasing method changes suddenly. If you are going to promote an element don’t wait until the animation starts, do it ahead of time.
Changing the transform of an element boils down to changes to its position, rotation or scale. Often, position is animated by setting the
top properties. The problem is, as shown above,
top both trigger layout operations, and that's expensive. The better solution is to use a
translate on the element, which does not trigger layout.
In Chrome Canary and Safari you can also animate filters, as these are handled off the main thread, are accelerated and generally perform very well. But since filters are not yet supported in Internet Explorer or Firefox you should proceed with caution.
Imperative vs Declarative Animations
Looking to the future
Animating well is core to a great web experience. You should always look to avoid animating properties that will trigger layout or paints, both of which are expensive and may lead to skipped frames. Declarative animations are preferable to imperative since the browser has the opportunity to optimize ahead of time.
Today transforms are the best properties to animate because the GPU can assist with the heavy lifting, so where you can limit your animations to these, do so.