A big part of accelerating websites is eliminating bottlenecks. Scripts are likely the most discussed bottlenecks, but CSS files are equally bad. CSS files will block all subsequent downloads if there’s a script (internal or external) in between – which is the case on practically any real page.
Most sites are affected by this bottleneck. Even extremely fast sites like webpagetest.org could still benefit from losing this “stair” in the waterfall.
So what can you do about it? You can load CSS asynchronously. Here are the key highlights of what you’ll need to do to achieve that, while maintaining the same site functionality. Alternatively, you can just use Blaze, as we’ve been applying this optimization for a while now
I’ll warn up-front that this isn’t trivial to do, but it’s definitely doable if your heart is set on it. Here’s the short list, details further down:
- Fetch the data for each CSS
- Apply CSS to the page
- Maintain the correct order
- Avoid FOUC by setting body visibility
Update: CSS files consistently block other resources on IE8 and often IE9, but not on Firefox and Opera. For WebKit, they only seem to block resources if they’re in the <head> of the document (likely a part of the preloader behavior). A cool and very simple hack found by Yoav Weiss is to add a div (or similar) element above the CSS links, which will make them not block. What it does is make WebKit (or at least Chrome) start the body earlier, and thus stop blocking. It’s super simple to do, just make sure you don’t create any side effects by ending the head early. Here are examples of CSS link in the head, in the body and in the head with Yoav’s trick.
It’s important to keep this array in the right order, since you’ll want to apply the CSS rules in the right order. In fact, sometimes your inline styles intentionally attempt to override external stylesheets. If that’s the case, those styles need to be placed in the relevant spot in the array as well.
2. Fetch the Data for Each CSS
It’s important to make sure you submit the requests in parallel, and that you start the requests as early as possible in the page.
3. Apply CSS to the Page
Once data is made available, you can apply it to the page (see caveat in #4). Applying CSS rules to the page isn’t always as easy as it sounds. The primary technique is to dynamically create a “style” tag, and populate its text. In IE, you need to change the style tag’s “styleSheet.cssText” attribute. I couldn’t find a good reference on how to do it, but bits and pieces are scattered around the web, if you know of some, please post them in the comments.
4. Maintain the Correct Order
Note that you may receive CSS responses in a different order than the order they were requested, so you can’t simply apply the data as you receive it. Each time you get new data, you should first store it in the relevant place in the array. Then traverse the array from the top, apply any element you got data for, and shift that item out of the array. This way you know you’re applying the data in the right order.
5. Avoid FOUC by setting body visibility
CSS Link tags, beyond blocking resources, also block rendering. On most sites, that’s a desirable behaviour, since rendering the site without the styling makes it look (and perhaps be) broken. Seeing the unstyled page is referred to as FOUC – Flash of Unstyled Content.
To avoid FOUC, you can manipulate the body’s “visibility” style. The initial HTML can have an invisible body (e.g. ), and once all the files are loaded, set the visibility to true.
Avoiding @import statements
Another scenario in which CSS blocks other resources is when using @import statements to load CSS files. There’s an old but good description of the different cases here, which is mostly up-to-date. Generally speaking, you should avoid using @import statements, since they make browsers load files sequentially instead of in parallel.
When using async CSS loading, avoiding @import resources is even more important. Depending on how the dynamic CSS is applied, @import statements will keep files from being downloaded in parallel at best, or modify the CSS order at worst.
The Inevitable Tradeoffs
Almost all performance optimizations come with a slight price. In the case of Async CSS, there are two tradeoffs to consider:
- Making CSS async may slightly delay the CSS downloads themselves. Since more resources will be downloaded in parallel, the CSS files will need to compete with the others for bandwidth. This is true for any optimization that does more in parallel, and from our experience it’s a non-issue for CSS given their small file size (especially after minification and compression)
- CSS files are naturally a Single Point of Failure since rendering is blocked until all CSS files are downloaded. If a CSS download fails, browsers will still display the other content. When implementing async CSS, you can apply the same behaviour, and can in fact enforce your own timeout.
Making it not block isn’t trivial, but it’s a worthwhile effort.