--- layout: post title: "CSS and Network Performance" date: 2018-11-09 19:57:12 categories: Web Development meta: "How can CSS impact network and resource-loading performance? Can things be that serious?" faq: - question: "Can CSS affect network performance?" answer: "Yes. CSS can delay rendering, block later requests, and influence how quickly the browser discovers and prioritises other resources." - question: "Is @import bad for performance?" answer: "Usually, yes. @import often creates additional request chains and can delay stylesheet discovery, which is why external stylesheets linked directly in HTML are generally preferable." - question: "Should I inline critical CSS?" answer: "Inlining a small amount of critical CSS can help the browser render above-the-fold content sooner by avoiding an extra render-blocking stylesheet request." - question: "Where should I place my stylesheet links?" answer: "That depends on the constraints, but in general you want to avoid accidentally blocking more important work and you should place stylesheets deliberately rather than by habit." --- Despite having been called CSS Wizardry for over a decade now, there hasn’t been a great deal of CSS-related content on this site for a while. Let me address that by combining my two favourite topics: CSS and performance. CSS is critical to rendering a page—a browser will not begin rendering until all CSS has been found, downloaded, and parsed—so it is imperative that we get it onto a user’s device as fast as we possibly can. Any delays on the Critical Path affect our Start Render and leave users looking at a blank screen. ## What’s the Big Problem? Broadly speaking, this is why CSS is so key to performance: 1. A browser can’t render a page until it has built the Render Tree; 2. the Render Tree is the combined result of the DOM and the CSSOM; 3. the DOM is HTML plus any blocking JavaScript that needs to act upon it; 4. the CSSOM is all CSS rules applied against the DOM; 5. it’s easy to make JavaScript non-blocking with `async` and `defer` attributes; 6. making CSS asynchronous is much more difficult; 7. so a good rule of thumb to remember is that **your page will only render as quickly as your slowest stylesheet**. With this in mind, we need to construct the DOM and CSSOM as quickly as possible. Constructing the DOM is, for the most part, relatively fast: your first HTML response _is_ the DOM. However, as CSS is almost always a subresource of the HTML, constructing the CSSOM usually takes a good deal longer. In this post I want to look at how CSS can prove to be a substantial bottleneck on the network (both in itself and for other resources) and how we can mitigate it, thus shortening the Critical Path and reducing our time to Start Render. ## Employ Critical CSS If you are able, one of the most effective ways to cut down the time to Start Render is to make use of the Critical CSS pattern: identify all of the styles needed for Start Render (commonly the styles needed for everything above the fold), inline them in ` {% endhighlight %} …will yield this waterfall:
Loss of parallelisation in Firefox due to ineffective Preload Scanner (N.B. The same waterfall occurs in IE/Edge.)
Here we can clearly see that the `@import`ed stylesheet does not start downloading until the JavaScript file has completed. The problem isn’t unique to JavaScript, either. The following HTML creates the same phenomenon: {% highlight html %} {% endhighlight %}
Loss of parallelisation in Firefox due to ineffective Preload Scanner (N.B. The same waterfall occurs in IE/Edge.)
The immediate solution to this problem is to swap the ` {% endhighlight %} There is a fascinating behaviour present in all browsers that is intentional and expected, yet I have never met a single developer who knew about it. This is doubly surprising when you consider the huge performance impact that it can carry: **A browser will not execute a ` {% endhighlight %} This is by design. This is on purpose. Any synchronous ` {% endhighlight %} Given this order, we can clearly see that the JavaScript file does not even begin downloading until the moment the CSSOM is constructed. We’ve completely lost any parallelisation:
Having a stylesheet before an async snippet undoes our opportunity to parallelise.
Interestingly, the Preload Scanner would like to have picked up the reference to `analytics.js` ahead of time, but we’ve inadvertently hidden it: `"analytics.js"` is a string, and doesn’t become a tokenisable `src` attribute until the `` blocks have no dependency on CSS, place them above your stylesheets.** Here’s what happens when we move to this pattern: {% highlight html %} {% endhighlight %}
Swapping a stylesheet and an async snippet around can regain parallelisation.
Now you can see that we’ve completely regained parallelisation and the page has loaded almost 2× faster. ### Place Any Non-CSSOM Querying JavaScript Before CSS; Place Any CSSOM-Querying JavaScript After CSS Dang. This article is getting way, way more forensic than I intended. Taking this even further, and looking beyond just async loading snippets, how should we load CSS and JavaScript more generally? To work that out, I posed myself the following question and worked back from there: If * synchronous JS defined after CSS is blocked on CSSOM construction, and; * synchronous JS blocks DOM construction… then—assuming no interdependencies—which is faster/preferred? * Script then style; * style then script? The answer: **If the files do not depend on one another, then you should place your blocking scripts above your blocking styles**—there’s no point delaying the JavaScript execution with CSS upon which the JavaScript doesn’t actually depend. (The Preload Scanner ensures that, even though DOM construction is blocked on the scripts, the CSS is still downloaded in parallel.) If some of your JavaScript does but some does not depend on CSS, then the absolute most optimum order for loading synchronous JavaScript and CSS would be to split that JavaScript in two and load it either side of your CSS: {% highlight html %} {% endhighlight %} With this loading pattern, we get download and execution both happening in the most optimum order. I apologise for the tiny, tiny details in the below screenshot, but hopefully you can see the small pink marks that represent JavaScript execution. Entry (1) is the HTML that is scheduled to execute some JavaScript when other files arrive and/or execute; entry (2) executes the moment it arrives; entry (3) is CSS, so executes no JavaScript whatsoever; entry (4) doesn’t actually execute until the CSS is finished.
How CSS can impact the points at which JavaScript executes.
**N.B.** It is imperative that you test this pattern against your own specific use-case: there could be different results depending on whether or not there are large differences in file-size and execution costs between your before-CSS JavaScript file and the CSS itself. Test, test, test. ## Place `` in `` This final strategy is a relatively new one, and has great benefit for perceived performance and progressive render. It’s also very component friendly. In HTTP/1.1, it’s typical that we concatenate all of our styles into one main bundle. Let’s call that `app.css`: {% highlight html %}

...

...
{% endhighlight %} This carries three key inefficiencies: 1. **Any given page will only use a small subset of styles found in `app.css`:** we’re almost definitely downloading more CSS than we need. 2. **We’re bound to an inefficient caching strategy:** a change to, say, the background colour of the currently-selected day on a date picker used on only one page, would require that we cache-bust the entirety of `app.css`. 3. **The whole of `app.css` blocks rendering:** it doesn’t matter if the current page only needs 17% of `app.css`, we still have to wait for the other 83% to arrive before we can begin rendering. With HTTP/2, we can begin to address points (1) and (2): {% highlight html %}

...

...
{% endhighlight %} Now we’re getting some way around the redundancy issue as we’re able to load CSS more appropriate to the page, as opposed to indiscriminately downloading everything. This reduces the size of the blocking CSS on the Critical Path. We’re also able to adopt a more deliberate caching strategy, only cache busting the files that need it and leaving the rest untouched. What we haven’t solved is the fact that it all still blocks rendering—we’re still only as fast as our slowest stylesheet. What this means is that if, for whatever reason, `site-footer.css` takes a long time to download, the browser can’t make a start on rendering `.site-header`. However, due to a recent change in Chrome (version 69, I believe), and behaviour already present in Firefox and IE/Edge, ``s will only block the rendering of subsequent content, rather than the whole page. This means that we’re now able to construct our pages like this: {% highlight html %}

...

...
{% endhighlight %} The practical upshot of this is that we’re now able to progressively render our pages, effectively drip-feeding styles to the page as they become available. In browsers that don’t currently support this new behaviour, we suffer no performance degradation: we fall back to the old behaviour where we’re only as fast as the slowest CSS file. For further detail on this method of linking CSS, I would recommend reading [Jake’s article on the subject](https://jakearchibald.com/2016/link-in-body/). {% include promo.html %} ## Summary There is a _lot_ to digest in this article. It ended up going way beyond the post I initially intended to write. To attempt to summarise the best network performance practices for loading CSS: * Lazyload any CSS not needed for Start Render: * This could be Critical CSS; * or splitting your CSS into Media Queries. * Avoid `@import`: * In your HTML; * but in CSS especially; * and beware of oddities with the Preload Scanner. * Be wary of synchronous CSS and JavaScript order: * JavaScript defined after CSS won’t run until CSSOM is completed; * so if your JavaScript doesn’t depend on your CSS; * load it before your CSS; * but if it does depend on your CSS: * load it after your CSS. * Load CSS as the DOM needs it: * This unblocks Start Render and allows progressive rendering. ### Warning Everything I have outlined above adheres to specs or known/expected behaviour, but, as always, test everything yourself. While it’s all theoretically true, things always work differently in practice. Test and [measure](/2018/10/three-types-of-performance-testing/). ### Thanks I’m grateful to [Yoav](https://twitter.com/yoavweiss), [Andy](https://twitter.com/andydavies), and [Ryan](https://twitter.com/ryantownsend) for their insights and proof-reading over the last couple of days.