Modern CSS has some great features for making user interfaces more pleasing and
understandable. One of these features is transitions. Transitions are a quick
way of animating a CSS property whenever its value is changed. They are
available for several properties such as transform
and color
.
Transitions and animations can be effective for communicating actions and contexts. They provide a visual narrative to help people understand how the state of the user interface is changing, and on a larger scale they help build continuity in the mental model the user has of your app’s landscape.
On a recent project, I discovered that our CSS declared site-wide transition
rules for all <a>
and <button>
elements.
a {
transition: color 0.5s;
}
button {
transition: background-color 0.5s;
}
In most cases this produces a desirable result. Small elements like text links and buttons transition their text or background color when the cursor hovers over them. This is more pleasing than simply changing the color immediately.
The cost of painting
During the transition, the browser is re-rendering the text dozens of times per second using a slightly different color each time. Rendering elements in the browser, or “painting”, is a process that requires optimization to keep our apps running quickly. If the browser repainted the entire screen every time a single element changed, we would notice a significant drop in performance. Fortunately for us, browsers are optimized to repaint only when necessary. However, there are sometimes situations where the browser can’t perfectly determine when to paint and when not to paint.
Repainting a small element like text during a half-second transition is no big deal, but what about larger elements? Performance impact is directly related to the area of the screen the browser must repaint, and the calculations required for painting. So what if the browser is repainting an element with lots of complicated CSS calculations, nested elements, or large images?
<a href="…">
<img src="a-big-image.jpg">
<div>
Some other things
</div>
</a>
The above HTML is an example of a pattern I came across in the same project.
Given what we know about the transitions on all <a>
elements, what do we
expect to happen? The browser was repainting the entire element at every step of
the transition.
I discovered this while using the aptly-named paint flashing tool in Firefox (Chrome has an equivalent tool). Every time an element is repainted, it also paints a semi-transparent, randomly-colored rectangle over the element. This is handy for identifying unnecessary or expensive paint calls (and having impromptu raves at your desk).
Making an exception to the rule
Instead of revisiting our site-wide link transition rule (which works as intended most of the time), we decided to create a utility class which we can use to remove all transitions from an element:
.u-no-transition {
transition: none !important;
}
This is one of the very few cases in which we should use !important
as part of
our rule, because some transition rules will likely have more specific selectors
than our utility class, and we want to override them.
Takeaway: keep an eye on browser performance using tools such as the profiler and paint flashing tool to avoid unnecessary rendering. Rendering affects not only performance, but also battery life.