Milica Mihajlija
September 23, 2019
Users want snappy and delightful experiences, which means running at 60 frames per second. To achieve this, you’ll have to know how browsers transform code to pixels on the screen and how styles can affect that process.
In this post you’ll learn how browser rendering works and how to navigate DevTools to diagnose animation performance issues. We’ll use Chrome DevTools, but other browsers have very similar features and the high-level concepts are the same everywhere.
To construct a web page, the browser has to go through a number of steps:
When the browser downloads and parses HTML and CSS, it creates the DOM (Document Object Model). To calculate the styles, the browser goes over all of the elements in the DOM and looks at the rules defined in stylesheets to figure out which CSS properties should be applied to each element.
To inspect the DOM and the calculated styles, open DevTools Elements panel, select an element, and navigate to the Computed section which shows the styles applied to it.
Once the browser knows which styles apply to each element, comes the layout phase where the browser figures out the dimensions and positions of all the elements it needs to display on the page. In order to that, it makes another tree — the layout tree. Elements in the layout tree are represented as vector boxes.
The layout tree is based on the DOM, but it’s usually shaped a bit different. The DOM contains all of the HTML nodes, but only the visible elements become part of the layout tree. Examples of things that the layout tree doesn’t include are:
Layout tree elements don’t map directly to the visible DOM elements. Large paragraphs of text—a single DOM element—are represented as multiple boxes in the layout tree. The layout tree also includes pseudo elements that are added to CSS (these are not present in the DOM).
In the layout phase, the browser calculates how much space each box is going to take and where to place it. It does that by looking at the viewport size and the computed styles. Some of the CSS properties that affect the layout are:
Once the layout has been established, the browser moves on to painting. It takes those vector boxes and turns them into pixels (also known as rasterizing). Paints are separated into individual layers, which can be re-painted without having to re-paint the entire page. This is especially useful to know in the context of animations.
Finally, the compositing step combines all layers into a single image that gets displayed on the screen.
Animations bring otherwise static content to life and help express brand personality. They make interactions feel more natural because we're hardwired to respond to moving objects. These animations need to be smooth and your page needs to be jank free. This is where your new knowledge about the rendering process can help you choose the best approach in coding.
When you add or remove elements from the DOM, animate their size or position the browser has to re-calculate the positions and sizes of elements in the document. This triggers layout.
For example, if you change the width of an element, any of its children may be affected and a big part of the page layout might change. Layout is almost always scoped to the entire document, so the larger the layout tree, the longer it takes to perform layout calculations.
And it is always followed by a paint.
When elements on the screen change, certain areas need repainting. But repaints are not always triggered by layout changes. The appearance of an element can be changed in a way that does not affect the layout. For example if you change the background color or outline, the size and position of the element aren’t changing, so there’s no need for the browser to do the layout—only repaint. And because the page is painted in layers, only parts of the screen need repainting.
To see how changing different CSS properties affects paint, layout and compositing, check out CSS Triggers.
When animating elements, it’s important to minimize layout and repaints. By taking advantage of the compositing process, you can create performant animations.
Modern browsers can animate four things really cheaply: position, scale, rotation and opacity.
Property | Value |
---|---|
Position | transform: translate(x, y) |
Scale | transform: scale(n) |
Rotation | transform: rotate() |
Opacity | opacity: 0..1 |
Browsers make optimizations by creating separate layers for elements with CSS transitions or animations on those four properties. When you have an animated element on its own layer, moving it around doesn’t affect the positions of surrounding elements, the only thing that moves is that layer. This way the browser avoids repaints and does only compositing.
The reason why compositing is a fast operation is that it happens on the GPU. GPU is a like a separate computer inside your device. It is a standalone unit with its own processors and its own memory. It’s designed for performing the complex mathematical calculations that are necessary for graphics rendering so it’s able to compose images very quickly, which takes the load off the CPU.
To animate an element in a separate layer with compositing, the browser has to be sure that the animated CSS property:
Otherwise, for every animation frame, the browser would have to recalculate the element’s geometry (do layout), render the image of the page’s new state (do repaint) and then send it again to the GPU to display on screen. With animated elements on separate layers, it’s just moving layers around or changing their opacity and that is exactly where the GPU shines.
With that in mind, here are some tips:
Animating elements with transform and opacity aren’t the only reasons for a browser to create separate layers. The way browsers split pages into layers is not standardized, so they can take slightly different approaches based on different heuristics.
Things that are likely to change, like elements with animated properties, usually get a separate layer. Different text boxes are usually painted on a single layer because they are likely to stay in place relative to each other.
Why isn’t everything in a separate layer? Well, apps (such as browsers or video games) have to communicate with the GPU as if it’s an external device. Transferring objects to the GPU takes time and each layer incurs a memory cost. This can be especially problematic on lower-end devices, so browsers have to balance between the GPU speed and the cost of processing layers.
To setup a successful animation performance analysis session, first enable the FPS meter.
The box in the top left corner provides real-time estimates for FPS as the page runs. Animations might be running smoothly at 60 FPS on your computer, but what do they look like on a less powerful device?
DevTools Performance panel is a treasure trove of information and it can simulate lower-end devices. To try it out:
Note: Depending on the device you are using, even 6x slowdown might be pretty fast, but you can customize this demo to make sure it’s putting strain your machine. Keep clicking Add 10 until the blue squares move noticeably slower than before. On a high-end machine, it may take about 20 clicks (credit: Kayce Basques).
If your frame rate is low or choppy, the Performance panel can help you identify the bottlenecks. While in the Performance panel press Command+Shift+E to start profiling and reload the page.
Note: When measuring performance, open Chrome in Incognito or Guest Mode. These modes ensure that Chrome runs in a clean state. For example, if you have a lot of extensions installed, those extensions might create noise in your performance measurements.
The Performance panel provides insight into all the work that browser is doing and what you get looks something like this:
The Main section shows activity that occurred on the page's main thread and the work is split into tasks. These include your JavaScript functions and internal tasks that browser performs in order to display a page.
The Summary tab at the bottom of the Performance panel sums up all this work in 6 categories:
The colors in the Summary tab correspond to the colors in the waterfall chart in the Main section.
To identify performance bottlenecks, look for long tasks in the DevTools Performance panel Timeline view. They are labeled with red flags and clicking on them reveals more details in the Summary tab.
Note: Animation performance can be affected by JavaScript, because style, layout and paint all happen on the browser’s main thread, alongside JavaScript execution. If the main thread is too busy running JavaScript, your animations can get slow and choppy.
Remember the rendering pipeline? Here’s a reminder:
parse → style → layout → paint → composite
Tasks related to animation performance are purple (style and layout) and green (painting and compositing). Try tracing a simple page so you can identify each of the steps from above in DevTools:
Zooming in reveals distinct tasks the browser goes through. To zoom in, in the Overview section, select a part of the Timeline with your mouse.
Once you have a section selected, you can drag the Timeline with the pointer and zoom in and out by scrolling. You can also use the W, A, S, and D keys to adjust your selection:
DevTools is also equipped with a neat feature for visualizing the painting process to help debug animation issues.
With paint flashing enabled, when you load a page, the entire screen flashes green because the browser has to paint everything. After the page has been rendered, the work is not done because things on the page can change. Each of these blinking animations requires the browser to paint a part of the screen, represented with a green rectangle.
Layout and repaints are expensive in terms of performance and can make your page slow. If you enable paint flashing and see the whole screen flash green, or areas of the screen that you didn’t think should be painted, you should dig in a little further.
For more details on the painting process, checkout the Paint profiler.
Clicking on a “Paint” task will bring up the Paint Profiler tab. It shows a view where you can see what got painted, how long it took, and the individual draw commands that the browser executed for the selected paint.
For this simple page, the browser drew one rectangle for the body with drawRect() and “Hi there!” with drawTextBlob().
You can see what your page’s layers look like in 3D in the Layers panel. To enable it, while in DevTools:
You can zoom, rotate, and drag the layers model to explore its contents. Hovering over a layer reveals its current position on the page. When you select a layer, the Details panel displays its memory consumption and the reason for the compositing. Keep the number of layers relatively low and watch out for memory hungry layers.
Now that you know about all the different tools for investigating rendering performance, go ahead and optimize your pages. To make sure your animations don’t cause performance issues, consider the impact of animating a given CSS property. Avoid layout recalculations and minimize repaints. Where you can, stick to changing transforms and opacity.
Happy coding!
Milica Mihajlija
Web Developer and Technical Writer. Find her on Twitter.
Join thousands of subscribers keeping up-to-date with web performance news and advice.
Addy Osmani
Engineering Manager at Google Chrome