Simplifying CSS Selectors

This post is based on a chapter from Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.



“Simplifying CSS Selectors” is the last chapter in my next book. My investigation into CSS selector performance is therefore fairly recent. A few months ago, I wrote a blog post about the Performance Impact of CSS Selectors. It talks about the different types of CSS selectors, which ones are hypothesized to be the most painful, and how the impact of selector matching might be overestimated. It concludes with this hypothesis:

For most web sites, the possible performance gains from optimizing CSS selectors will be small, and are not worth the costs. There are some types of CSS rules and interactions with JavaScript that can make a page noticeably slower. This is where the focus should be.

I received a lot of feedback about situations where CSS selectors do make web pages noticeably slower. Looking for a common theme across these slow CSS test cases led me to this revelation from David Hyatt’s article on Writing Efficient CSS for use in the Mozilla UI:

The style system matches a rule by starting with the rightmost selector and moving to the left through the rule’s selectors. As long as your little subtree continues to check out, the style system will continue moving to the left until it either matches the rule or bails out because of a mismatch.

This illuminates where our optimization efforts should be focused: on CSS selectors that have a rightmost selector that matches a large number of elements in the page. The experiments from my previous blog post contain some CSS selectors that look expensive, but when examined in this new light we realize really aren’t worth worrying about, for example, DIV DIV DIV P A.class0007 {} . This selector has five levels of descendent matching that must be performed. This sounds complex. But when we look at the rightmost selector, A.class0007 , we realize that there’s only one element in the entire page that the browser has to match against.

The key to optimizing CSS selectors is to focus on the rightmost selector, also called the key selector (coincidence?). Here’s a much more expensive selector: A.class0007 * {} . Although this selector might look simpler, it’s more expensive for the browser to match. Because the browser moves right to left, it starts by checking all the elements that match the key selector, “ * “. This means the browser must try to match this selector against all elements in the page. This chart shows the difference in load times for the test page using this universal selector compared with the previous descendant selector test page.

Load time difference for universal selector

It’s clear that CSS selectors with a key selector that matches many elements can noticeably slow down web pages. Other examples of CSS selectors where the key selector might create a lot of work for the browser include:

A.class0007 DIV {} #id0007 > A {} .class0007 [href] {} DIV:first-child {}

Not all CSS selectors hurt performance, even those that might look expensive. The key is focusing on CSS selectors with a wide-matching key selector. This becomes even more important for Web 2.0 applications where the number of DOM elements, CSS rules, and page reflows are even higher.