Avoiding paints is critical to achieving a silky smooth frame rate, especially on mobile. Sometimes, however, paints crop up in the most unusual of places. This article looks at why animated GIFs can cause unnecessary paints to occur, and the remarkably simple fix you can apply.
Layers of loveliness
As you probably know, modern browsers may paint groups of DOM elements into separate "images", called layers. Sometimes there's one layer for the entire page, sometimes there are hundreds or in rare cases - thousands!
When DOM elements are grouped together into a layer and one of the elements changes visually, we end up having to paint not just the changed element, but all the other elements in the layer that overlap the changed element as well. Painting one thing on top of another results in the overwritten pixels being effectively "lost" forever; if you want the original pixels back you need to repaint them.
Sometimes, therefore, we want to isolate one element from the others so that when it gets painted we won't need to repaint the other elements that haven't changed. For example, when you combine a fixed page header with scrollable content, you have to repaint the header each time the content scrolls, as well as the newly visible content. By placing the header in a separate layer, the browser can optimize scrolling. When you scroll, the browser can move the layers around - probably with the help of the GPU - and avoid repainting either layer.
Each additional layer increases memory consumption and adds performance overhead, so the goal is to group the page into as few layers as possible while maintaining good performance.
What does this all have to do with animated GIFs?
Well let's have a look at this picture:
This is a potential layer setup for a simple app. There are four layers here: three of them (layers 2 to 4) are interface elements; the back layer is a loader, which happens to be an animated GIF. In the normal flow you show the loader (layer 1) while your app loads, then as everything finishes you would show the other layers. But here's the key: you need to hide the animated GIF.
But why do I need to hide it?!
Good question. In a perfect world the browser would simply check the GIF’s visibility for you and avoid painting automatically. Unfortunately, checking whether the animated GIF is obscured or visible on the screen is typically more expensive than simply painting it, so it gets painted.
In the best case the GIF is in its own layer and the browser only has to paint and upload it to the GPU. But in the worst case all your elements might be grouped into a single layer and the browser has to repaint every single element. And, when it’s done, it still needs to upload everything to the GPU. All of this is work occurs for every GIF frame, despite the fact that the user can’t even see the GIF!
On desktops you can probably get away with this kind of painting behavior because the CPUs and GPUs are more powerful, and there's plenty of bandwidth for transferring data between the two. On mobile, however, painting is extremely expensive so you must take great care.
Which browsers does this affect?
As is often the way, behaviors differ between browsers. Today Chrome, Safari and Opera all repaint, even if the GIF is obscured. Firefox, on the other hand, figures out that the GIF is obscured and doesn’t need to be repainted. Internet Explorer remains something of a black box, and even in IE11 - since the F12 tools are still being developed - there is no indication as to whether or not any repainting is taking place.
How can I tell if I have this problem?
The easiest way is to use "Show paint rectangles" in Chrome DevTools. Load DevTools and press the cog in the lower right corner () and choose Show paint rectangles under the Rendering section.
Now all you need to do is look for a red rectangle like this:
display: none or
visibility: hidden to it or its parent element. Of course if it's just a background image then you should make sure to remove it.
If you want to see an example of this behavior in a live site, check out Allegro, where each product’s image has a loader GIF that is obscured rather than explicitly hidden.
Achieving 60fps means doing only what's needed to render the page and no more. Removing excess paints is a critical step in achieving this goal. Animated GIFs that are left running can trigger unnecessary paints, something which you can find and debug easily with DevTools' Show paint rectangles tool.
Now, you didn't leave that animated kitten loader GIF running forever, did you?