Simplifying CSS Selectors

June 18, 2009 12:55 pm | 25 Comments

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

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.

25 Responses to Simplifying CSS Selectors

  1. Thank you! This is exciting information. I always assumed CSS was interpreted left to right. It’ll be interesting to create CSS for a new site template with this in mind. It’d be harder to retool an existing one, I think.

  2. I predict that combinators also significantly impact performance. E.g. ‘A.class0007 > *’ will be matched faster than ‘A.class0007 *’ since the former can terminate at the parent, while the latter must check every ancestor up to the root to discover a failed match. (‘+’ and ‘~’ also share the same relationship.) Although the number of ancestors per element is usually small, there will be a high multiplier if the rightmost simple selector is generic.

    To my understanding, DOM traversal (matching across combinators) is typically more expensive than matching simple selectors (the bits in between combinators), so reducing/optimising combinators should yield even greater performance gains.

    Especially poor-performing selectors can be crafted by combining the recursive combinators, e.g. ‘a ~ b c’, so you definitely should avoid placing generic simple selectors here (e.g. ‘A.class0007 ~ * *’). You really need to know a lot about the implementation to understand why to avoid these, so Steve if you learn any more advice from Gecko/WebKit implementors, I’d really appreciate knowing about it! :-)

  3. Does anyone know why selector engines work from right to left?

    For the type of selectors I most commonly write (and I suspect others) it would be more optimal to go in the other direction.

    For instance it is common to target descendants like ‘#id div’.

    That would be slower going right to left than left to right. But is a *lot* more useful than ‘div #id’, which is rarely useful, or intuitive, to someone writing css

  4. @Scott: Combinators increase the cost of matching, but the most important place to focus is the rightmost selector (the key selector). As I point out in this post, you can have a selector with lots of combinators, but if the key selector only matches one element in the page, the cost of matching is small.

    @Pete: David Baron is the CSS guy at Mozilla. In the video of his Google tech talk Faster HTML and CSS, at time 21:00, he touches on this. It’s not a full explanation, but he explains that internally the browser contructs a hash for doing the selector matching. I believe that if the matching was performed left-to-right, the browser would have to use much more memory for these hashes.

  5. Rather than doing:

    * { margin: 0, padding: 0 }

    Would you suggest a reset.css instead?

  6. @James: That single universal simple selector is going to be faster than downloading a stylesheet.

  7. @Pete: I’ve tried to investigate right-to-left and left-to-right problem in YASS (CSS3 selectors JavaScript library, the fastest and the smallest). For simple cases (CSS1 specification) it can be 20-50% faster to use right-to-left notation (even through browsers DOM API) — this can be checked via YASS CSS1 branch.

    For CSS3 selectors we don’t have such advance, so both directions are almost equal in terms of performance. The problem is that CSS3 selectors are used much lesser than CSS1 ones. And of course you can notice that most of CSS3 selectors are slow in all modern browsers.

    @Steve: I think you need to include some words aboud CSS3 selectors and their performance. It’s important for all web developers to understand that:
    .a p
    will work faster than
    .a p:first-child
    or
    .a p:nth-child(odd)
    if we have only 1 p inside div.a

    Also there should be some recommendations about using #id and .classes. And level of CSS selectors — what is faster:
    .a p a span
    or
    #span_inside_div_class_a
    ? And when we will have 1000 such spans?

    There are a lot of issues with CSS3 selectors, but most of them are not significant for simple websites or possible optimization will give us 1-2ms for a page. And there is no complete information about this.

  8. Great comments sunnybear. I cover this and more in the book chapter, but at 13 pages it’s too long for a blog post. There certainly are real world cases where inefficient CSS selectors have slowed down pages by multiple seconds. It’s important to focus on the ones that are the most costly, and not change your CSS willy nilly. The key is the key selector.

  9. @Steve: I will be glad to participate somehow in your coming book :) As I’ve already commented previous article about selectors — I investigated this problem a year ago. There are several articles in Russian.
    http://webo.in/articles/habrahabr/19-css-efficiency-tests/
    http://webo.in/articles/habrahabr/25-css-efficiency-tests-2/
    http://webo.in/articles/habrahabr/38-css-efficiency-tests-3/
    http://webo.in/articles/habrahabr/53-semantic-dom-tree/
    If you need any clarifications in English — please ask :)

  10. Hey Steve,
    Is the universal selector page apples to apples against the descendant selector test page? The universal selector page isn’t matching any elements, and no background color is applied, while the descendant selector matches all the a tags. Shouldn’t they provide the same result, if you’re trying to compare the two?

  11. @Zach: I had a typo during my last edits, but the tests were done with the proper application of styles, so the performance numbers are valid.

  12. The * css reset might be faster than downloading a stylesheet, but what about reflow? I think it slows down pages a lot, especially ones with javascript animations.

  13. I can foresee a lot of miconceptions arising from this post. Not your fault Steve, but it will happen.

    First, it is *internal* CSS selectors that are evaluated from right to left. This affects CSS rendering and not JavaScript.

    Second, JS libraries execute CSS queries from *left to right*. That is the way that the DOM APIs work. Some clever libraries may optimise their queries but generally they are left to right.

    For people reading this post to with the intent to speed up their JavaScript, go back to thinking “left to right” in order to optimise your queries.

  14. @Dean: left-to-right isn’t just ‘DOM APIs’ issue. It’s just the way most of JS libraries implement this. You can implement right-to-left in the same manner and get your our JS CSS Selectors library.

    But right-to-left ‘native CSS’ way is hardcoded in browsers. And we can’t change this anyway.

  15. @Dean: Thanks for your comment. To be clear, this blog post is about CSS performance, so thinking right to left is the place to focus.

  16. Excellent article that gave me pause to think. Do you suppose complex and bloated CSS can cause some pages to scroll in jerky movements instead of smoothly?

    I find that in Firefox, some pages scroll very poorly, but the same pages scroll smoothly in IE and Safari.

    Thoughts?

  17. That chart is really interesting, but hard to evaluate without the baseline number. For example, over here I can confirm that for Firefox 3.5 the difference between the two testcases is larger than for Safari 4, but that’s because it’s 4x faster than Safari 4 on the “Descendant” testcase and only 2x faster on the “Universal” one… It would have been more interesting, to me, to see both numbers presented on a single bar chart instead of the difference being presented.

  18. David Hyatt’s article is a good one. But is there any evidence that Trident and Webkit read selectors right to left as well?

  19. @Will: From a blackbox perspective, I would say yes. The test pages used in this blog post would be trivial if selectors were applied left to right, and yet they still take a long time in those other browsers.

  20. While this is very interesting and I will put it to practice in the future, I must point out that the poor css is hardly the bottleneck between a site and its visitors.

    For example: You could write all the clean code you want, but one poorly compressed image can eat up all your gains and then some!

  21. I love this kind of post. But like someone else mentions the power of css, to me is reading from left to right. Adding additional classes and ids, just to narrow down the keyselector. Feels wrong.

  22. I am not trying to troll or anything but those tests are BS, whilst I agree with what you are saying (its fact that it goes right to left), you use the * once on multiple elements, not * multiple times on multiple elements.

    I say this because I use the * rule as a simple reset when prototyping, and well, the speed difference in those tests with 1 star rule vs 1 specific rule = none, infact the star rule seems to have the ability of being faster than the specific one… I downloaded the two HTML pages and swapped the 1000 css entries with:

    DIV DIV DIV P A { background: #CFD; }
    vs
    P * { background: #CFD; }

    i mean, really, there is literally no difference, and as I said sometime star is faster… so well, whatever :`s thanks for the post all the same, and well anyone that uses the star rule lots probably deserves a slow page (that sounds evil, but I dont mean it to be!)

  23. You may like to check new SP plugin about optimization – http://www.satya-weblog.com/2010/08/website-performance-optimization-plugin-ordering-stylesheet-javascript.html

  24. haha, that chart pretty much sums up browsers in badness with IE6 the “winner” by far. very interesting read, thanks

  25. That’s what I like about using SASS, if you use the nesting functionality it creates CSS with all the elements listed, ensuring the nesting goes far enough, surely the performance of the site load is improved. Good article.