SCSS and LESS have made waves in the frontend development community. They’ve allowed us to write concise styles in a lean fashion, but is there a hidden performance tax?

The fact is preprocessors are used widely at the production level. But what about performance? What comes out is simply CSS, but how does that CSS compare to what we used to write by hand? How do browsers cope with the reams of machine generated styles?

In this post I’ll investigate how common CSS preprocessing idioms can affect the browser’s rendering performance.

The anatomy of a web-request; load times beyond the first hit

The accepted wisdom recommends combining your assets together into a single large file where possible. Browsers open a limited number of concurrent requests to the server, so given the speed of connections, and the diminutive file size thanks to GZIP, you may as well send as much data as you can on that initial connection [1]. Thus, if we’re to follow this thinking, there are two categories of requests and optimizations that can be made:

Initial requests: This is well covered ground. Suffice to say you will make more substantial gains ensuring your compression, CDNs, sprites, minification and the like are functioning optimally than worrying about the structure of your CSS.

Subsequent requests: When you’ve established a baseline of performance with initial requests, further optimizations will bring you here. Let’s break down the order of page rendering.

In this subsequent request to bindle.me, we see that although the HTML has been returned after 624ms, the DOM is not ready for around 1.5s. Although much of this time is taken parsing JS, it seems that there is scope for gains to made in rendering time.

  1. Download time: The browser hits your server up for the URL. Hopefully you are fast enough that the HTML is returned to the browser within a few hundred ms.
  2. Cached? The browser looks at what assets it needs, apart from a few images that aren’t important to the layout of the page, everything is in cache (0ms).
  3. Parse time: The browser parses the CSS [2] of the page and is ready to render.
  4. Render time: The browser renders the HTML, using the CSS.
  5. Ready to go: The page looks correct (apart from maybe a few images which will slot in later) and is ready to use.

 

In the first request, the amount of waiting is determined mostly by whether assets need to be downloaded. For subsequent requests, the parse and render time can take as long, if not longer than download time. So subsequent requests are definitely a concern if you want to keep your web app snappy.

The double edged sword: How SCSS can help & hinder 

At Bindle we have ~47 icons which have 4 states each. Each of these combinations requires it’s own style rule like so:

a:active.close.icon16:before {
 background-position: -425px -34px;
}

47*4 = 188 styles at the very least. Of course it’s not that simple.

We need .close.icon16:before to take a different state depending on whether it’s a link or a button, which state it’s in (:hover, :active, :focus), whether it has class inverse or checked or current, if it’s in the #sidebar etc etc…  We generate ~1000 icon rules, each of which is a single background-position rule [3]. This kind of bloat would not have been possible without CSS preprocessors.

Believe it or not, we had more like 3000 at one point. We’ve pared it down since, but should we add more icons or states we may be overwhelmed. This was not the goal of CSS at it’s inception as evidenced by IE’s lingering problem with too many styles, and obviously it adds to the stylesheet’s size, but how much does it affect CSS rendering time?

Aside 1:  ‘Ideal’ HTML + CSS

Purists say that we should separate our form from function. CSS was originally developed in part to help us say what we mean in the HTML and to specify what it looks like in the stylesheet. This step enabled us to go from:

<a font-face="Helvetica" color="red>

To

<a class="fancy red">

A massive improvement. However, it isn’t very semantic to say “This <a> is red and fancy”. It’s better to say “This <a> is a call to action”:

<a class="call-to-action">

And to make the .call-to-action class red and fancy:

.call-to-action {
  // CSS preprocessors let us do this; now we are getting somewhere.
  @include red;
  @include fancy;
}

Is this the correct solution though? Is the HTML the right place to be saying this particular <a> is the CTA on this page? Isn’t this a style issue that may change? (Perhaps we have different priority on different @media devices?). Isn’t it better to leave the <a> clean and say:

#sidebar a:first-child {
  @include call-to-action;
}

“All first links in the sidebar are call-to-action”. Then we can get specific, and override it on the users#show page [4]:

.c_users.v_show #sidebar a:last-child {
  @include call-to-action;
}

In this post we will see that although this style of CSS may be ‘more progressive’; it isn’t a habit you want to form and will hurt performance if you’re careless.

Aside 2: How browsers parse and render CSS

Disclaimer: I am not an expert on this; and perhaps it may change in the future.

Browsers parse CSS from right to left

The ‘parsing’ step of CSS rendering involves constructing a massive hash table of right-hand-side(RHS) selectors. So when a browser sees:

.c_users.v_show #sidebar a { ... } //[1]

It knows that this rule will only apply to <a> elements, and no others.

When rendering, the browser runs through elements one at a time, testing all such rules that match. So every time it finds a link element, it must check to see if it is a descendant of a #sidebar inside a .c_users.v_show. This is a relatively expensive check as it requires running up the whole DOM tree; so we can see that the right-hand side selector is very important.

So every time you construct a rule that has a overly generic selector on the RHS (an <a> rather than a .call-to-action say), you add an extra check that must be made for every single element on the page that matches.

Quantifying Rendering time

In an effort to try and quantify this CSS rendering time I’ve constructed a set of experiments that measure the parsing + rendering time for a very large test page. These should give you an idea of what to expect, and how much credence you should put into worrying about parsing.

Read the source code to the css-timings project and give it a try. I ran these experiments on my late 2011 Macbook Pro and my iPhone 4 [5] and recorded the results in a public google doc. I’ll explain the setup here and talk through the significance of the results in a moment.

The basic experiment is as follows; our page consists of 10,000 <div>s, each with a unique class name. Our base CSS file consists of one simple style targeted to each of the <div>s (so 10,000 base styles as well).

Experiment One: How long does it all take?

10,000 elements on one hand is a lot. Steve Souders measured the number of selectors on some popular sites and averaged around 1,000 back in 2009. I’d probably estimate that this has grown by now. However it helps us emphasize differences.

10,000 style rules on the other hand isn’t that many. We (Bindle) have around 6,000 selectors right now, and most have many more than 1 rule.

There is a fairly linear relationship between the number of elements of the page and the rendering time when there are many potentially applicable style rules per element.

Anyway, given those caveats, parsing & rendering these elements takes a non-trivial but probably insignificant amount of time on my high-end laptop; however, the rendering time on Mobile Safari is much more noteworthy.

Conclusion #1: Mobile Devices are slow enough that this stuff actually matters.

Experiment Two: What difference do inapplicable styles make?

All browsers seem to have a similar profile; they suffer greatly with overly generic RHS selectors.

You’ve combined all your CSS files into one big file, so when rendering each page of the site the browser must consider the styles which are targeted at all your other pages. Not to worry, you use a nice CSS namespacing library [6], so there’s no danger of it getting confused. What does checking all those extra styles cost?

Here we experiment with adding an extra 10,000 irrelevant style rules. Firstly we can try to add styles that the browser knows won’t apply, either because they target the wrong element, or because they target a non-existent class. We see that they make a linear difference to CSS parsing time—parsing twice as many styles takes twice as long, surprisingly enough—and no difference to rendering time. Well done browser!

On the other hand, if the styles don’t apply, but do match on the RHS, for instance like:

#bla .foo ul .something-else div

The browser is forced to do many expensive ancestor checks, and the rendering time blows way out (even in the seconds for Firefox on my Pro).

Conclusion #2: Try to right-most target classes where possible.

This, along with the more pressing issue of CSS file size, is a nail in the coffin for the idea of ‘replacing’ classes with @mixins and writing super-semantic html.

Experiment Three: can we hide irrelevant rules from the browser?

Seeing as your stylesheet is no doubt full of rules that you know are never going to apply to the page being rendered, you can’t help but wish there way some way to tell the browser not to worry about them. We understand the reason why browsers look at rules right-to-left, but is there something that we can put on the LHS to help the browser out?

Unfortunately, even if you explicitly tell the browser that the body needs a specific class for the rule to apply:

body.c_users.v_show #sidebar > * { .. }

Browsers aren’t smart enough to work in such a left-to-right fashion [7]. One trick that does work is to use a @media query:

@media-type print {
  #sidebar > * { .. }
}

Browsers do disregard those rules entirely. However, there’s no ‘custom’ media queries available, so we cannot do something like:

@media-page v_users_c_show {
  #sidebar > * { .. }
}

Conclusion #3: At this point, there is no good way to hide styles. So we need to be careful.

Conclusion: CSS efficiency can have an impact on mobile; but only if you do things really wrong.

So, after all this, what can we conclude? Firstly, hopefully the css-timings project can provide a nice base for people to conduct their own experiments and try to measure the impact of various other CSS quirks they may be thinking of squashing. Please feel free to criticize my methodology and conclusions to your hearts content!

Secondly, poor CSS will hurt you on mobile. The amount any one mistake can hurt you, though small, will add up with systemic errors like global selector * rules:

.foo #bar > *

Each will add 0.15ms to the rendering time of every single page on your entire website [8]. So don’t worry about a single rule. However, systemic use will lead to a constant rendering tax that will adversely effect the user experience.

So, I would advise:

  1. Steer clear of replacing classes with @mixins. One day the browsers will support it or something like it, but until then,
    isn’t such a horrible mixture of presentation and content.
  2. Keep your number of selectors down, but don’t go overboard about it. This post should hopefully give you an idea of you are dealing with.
  1. with the benefit of all subsequent request being cached in the browser []
  2. and JS if you don’t put it at the bottom of the <body>. I’ll leave this discussion for another time []
  3. Thanks Mozilla! We could cut this down to about 50 if you supported background-position-y. []
  4. Here and in future I use the syntax provided by edifice for namespacing page-level CSS []
  5. Note that these experiments are very CPU bound. On Dom’s early 2010 Macbook pro (18 months older), we saw pretty much 2x times for more-or-less everything. Moore’s Law in action, baby! []
  6. Say, edifice again? []
  7. It could work though. There can only be one body in the page, so if the body doesn’t have the relevant classes, we can ‘turn that rule off’. []
  8. Where did I get 0.15ms from? We saw that 10k over-general selectors adding on 10k elements blew the time out by 15s. So, given that’s 100 million ‘checks’, each check takes around 0.00015ms. If we have an average sized page (one thousand elements), then each * selector will cause 1000 checks, and thus take around 0.15ms. YMMV []
Tom Coleman

Co-creator of bindle.me, searching for simplicity, quality and elegance in technology, products and code.

See all Tom Coleman's posts

5 Responses to “Is SCSS killing your site’s performance?”

  1. Dan Dascalescu says:

    Interesting post, and well-designed experiment.

    PS: one typo, “<a class="fancy red"”

  2. [...] with massive reflows, lots of old browsers and complex mobile requirements, most  sources [1, 2, 3, 4] make me believe that heavily optimizing for CSS performance, isn’t really worth the [...]

  3. Zoltan Olah says:

    Great post Tom, I know we’ve pondered about this in the past – i’m glad to see you’ve gone and actually measured it.

    • Tom Coleman says:

      Cheers- yeah, the results aren’t exactly unexpected, but it’s one of those things; you make assumptions about this stuff but you never really know.

      I guess I’ll have to think about doing the same thing with JS next I guess. I think the big gain to be made there is putting it at the bottom of the HTML (so it parses after the DOM has loaded). The issue is of course that code relies on ondomready events etc. But again, it’d be good to know what gains could be made..

Leave a Reply

  • Search: