Reduce the scope and complexity of style calculations

JavaScript is often the trigger for visual changes. Sometimes that's directly through style manipulations, and sometimes it's calculations that will result in visual changes, like searching or sorting some data. Badly-timed or long-running JavaScript can be a common cause of performance issues, and you should look to minimize its impact where you can.

Changing the DOM, through adding and removing elements, changing attributes, classes, or through animation, will all cause the browser to recalculate element styles and, in many cases, layout (or reflow) the page, or parts of it. This process is called computed style calculation.

The first part of computing styles is to create a set of matching selectors, which is essentially the browser figuring out which classes, pseudo-selectors, and IDs apply to any given element.

The second part of the process involves taking all the style rules from the matching selectors and figuring out what final styles the element has.

Summary

  • How reducing style calculation costs can lower interaction latency.
  • Reduce the complexity of your selectors; use a class-centric methodology (BEM, for example).
  • Reduce the number of elements on which style calculation must be calculated.

Style recalculation time and interaction latency

The Interaction to Next Paint (INP) is a user-centric runtime performance metric that assesses a page's overall responsiveness to user input. When interaction latency is assessed by this metric, it measures the time starting from when the user interacts with the page, up until the browser paints the next frame showing the corresponding visual updates made to the user interface.

A significant component of an interaction is the time it takes to paint the next frame. Rendering work done to present the next frame is made up of many parts, including calculation of page styles that occur just prior to layout, paint, and compositing work. While this article focuses solely on style calculation costs, it's important to emphasize that reducing any part of the rendering phase inherent to interaction will reduce its total latency—style calculation included.

Reduce the complexity of your selectors

In the simplest case, you can reference an element in your CSS with just a class:

.title {
  /* styles */
}

But, as any project grows, it will likely result in more complex CSS, such that you may end up with selectors that look like this:

.box:nth-last-child(-n+1) .title {
  /* styles */
}

In order to know how these styles apply to the page, the browser has to effectively ask "is this an element with a class of title which has a parent who happens to be the minus nth child plus 1 element with a class of box?" Figuring this out can take a lot of time, depending on the selector used as well as the browser in question. The intended behavior of the selector could instead be changed to a class:

.final-box-title {
  /* styles */
}

You can take issue with the name of the class, but the job just got a lot simpler for the browser. In the previous version, in order to know, for example, that the element is the last of its type, the browser must first know everything about all the other elements and whether the are any elements that come after it that would be the nth-last-child, which is potentially more expensive than simply matching up the selector to the element because its class matches.

Reduce the number of elements being styled

Another performance consideration—which is typically the more important factor for many style updates—is the sheer volume of work that needs to be carried out when an element changes.

In general terms, the worst case cost of calculating the computed style of elements is the number of elements multiplied by the selector count, because each element needs to be at least checked once against every style to see if it matches.

Style calculations can often be targeted to a few elements directly rather than invalidating the page as a whole. In modern browsers, this tends to be less of an issue because the browser doesn't necessarily need to check all the elements potentially affected by a change. Older browsers, on the other hand, aren't necessarily as optimized for such tasks. Where you can, you should reduce the number of invalidated elements.

Measure your style recalculation cost

One way to measure the cost of style recalculations is to use the performance panel in Chrome DevTools. To begin, open DevTools, go to the tab labeled Performance, hit record, and interact with the page. When you stop recording, you'll see something like the image below:

DevTools showing style calculations.

The strip at the top is a miniature flame chart that also plots frames per second. The closer the activity is to the bottom of the strip, the faster frames are being painted by the browser. If you see the flame chart leveling out at the top with red strips above it, then you have work that's causing long running frames.

Zooming in on a trouble area in Chrome DevTools in the activity summary of the populated performance panel in Chrome DevTools.

If you have a long running frame during an interaction like scrolling, then it bears further scrutiny. If you have a large purple block, zoom in on the activity and select any work labeled Recalculate Style to get more information on potentially expensive style recalculation work.

Getting the details of long-running style calculations, including vital information such as the amount of elements affected by the style recalculation work.

In this grab there is long-running style recalculation work that is taking just over 25ms.

If you click the event itself, you are given a call stack. If the rendering work was due to a user interaction, the place in your JavaScript that is responsible for triggering the style change will be called out. In addition to that, you also get the number of elements that have been affected by the change—just over 900 elements in this case—and how long it took to do the style calculation work. You can use this information to start trying to find a fix in your code.

Use Block, Element, Modifier

Approaches to coding like BEM (Block, Element, Modifier) actually bake in the selector matching performance benefits above, because it recommends that everything has a single class, and, where you need hierarchy, that gets baked into the name of the class as well:

.list {
  /* Styles */
}

.list__list-item {
  /* Styles */
}

If you need some modifier, like in the above where we want to do something special for the last child, you can add that like so:

.list__list-item--last-child {
  /* Styles */
}

If you're looking for a good way to organize your CSS, BEM is a good starting point, both from a structure point-of-view, and also because of the simplifications of style lookup the methodology promotes.

If you don't like BEM, there are other ways to approach your CSS, but the performance considerations should be assessed alongside the ergonomics of the approach.

Resources

Hero image from Unsplash, by Markus Spiske.