CSS and Network Performance – CSS Wizardry – CSS Architecture, Web Performance Optimisation, and more, by Harry Roberts
Written by Harry Roberts on CSS Wizardry.
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:
- A browser can’t render a page until it has built the Render Tree;
- the Render Tree is the combined result of the DOM and the CSSOM;
- the CSSOM is all CSS rules applied against the DOM;
- making CSS asynchronous is much more difficult;
- 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
(both in itself and for other resources) on the network, and how we can mitigate
it, thus shortening the Critical Path and reducing our time to Start
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
<style> tags in the
<head> of your document, and
asynchronously load the remaining stylesheet off of the Critical Path.
While this strategy is effective, it’s not simple: highly dynamic sites can be
difficult to extract styles from; the process needs to be automated; we have to
make assumptions about what above the fold even is; it’s hard to capture edge
cases; tooling still in its relative infancy. And if you’re working with a large
or legacy codebase, things get even more difficult…
So if achieving Critical CSS is proving quite tricky—and it probably is—another
option we have is to split our main CSS file out into its individual Media
Queries. The practical upshot of this is that the browser will…
- download any CSS needed for the current context (medium, screen size,
resolution, orientation, etc.) with a very high priority, blocking the
Critical Path, and;
- download any CSS not needed for the current context with a very low priority,
completely off of the Critical Path.
Basically, any CSS not needed to render the current view is effectively
lazyloaded by the browser.
<link rel="stylesheet" href="all.css" />
If we’re bundling all of our CSS into one file, this is how the network treats
If we can split that single, all-render blocking file into its respective Media
<link rel="stylesheet" href="all.css" media="all" /> <link rel="stylesheet" href="small.css" media="(min-width: 20em)" /> <link rel="stylesheet" href="medium.css" media="(min-width: 64em)" /> <link rel="stylesheet" href="large.css" media="(min-width: 90em)" /> <link rel="stylesheet" href="extra-large.css" media="(min-width: 120em)" /> <link rel="stylesheet" href="print.css" media="print" />
Then we see that the network treats files differently:
The browser will still download all of the CSS files, but it will only block
rendering on files needed to fulfil the current context.
@import in CSS Files
The next thing we can do to help Start Render is much, much simpler. Avoid
@import in your CSS files.
@import, by virtue of how it works, is slow. It’s really, really bad for Start
Render performance. This is because we’re actively creating more roundtrips on
the Critical Path:
- Download HTML;
- HTML requests CSS;
- (Here’s where we’d like to be able to construct the Render Tree, but;)
- CSS requests more CSS;
- build the Render Tree.
Given the following HTML:
<link rel="stylesheet" href="all.css" />
…and the contents of
…we end up with a waterfall like this:
By simply flattening this out into two
<link rel="stylesheet" /> and zero
<link rel="stylesheet" href="all.css" /> <link rel="stylesheet" href="imported.css" />
…we get a much healthier waterfall:
N.B. I want to briefly discuss an unusual edge case. In the unlikely
event that you don’t have access to the CSS file that contains the
(meaning you’re unable to delete it), you can safely leave it in place in the
CSS but also complement it with the corresponding
<link rel="stylesheet" />
in your HTML. This means that the browser will initiate the imported CSS’
download from the HTML and will skip the
@import: you won’t get any double
@import in HTML
This section is odd. Very odd. I disappeared down such a huge rabbit hole
researching this one… Blink and WebKit are broken because of a bug; Firefox and
IE/Edge just seem broken. I’m filing the
To fully understand this section we first need to know about the browser’s
Preload Scanner: all
major browsers implement a secondary, inert parser commonly referred to as the
Preload Scanner. The browser’s primary parser is responsible for constructing
different part of the document block it. The Preload Scanner can safely jump
ahead of the primary parser and scan the rest of the HTML to discover references
to other subresources (such as CSS files, JS, images). Once they’ve been
discovered, the Preload Scanner begins downloading them ready for the primary
parser to pick them up and execute/apply them later. The introduction of the
Preload Scanner improved web page performance by around 19%, all without
developers having to lift a finger. This is great news for users!
One thing we as developers need to be wary of is inadvertently hiding things
from the Preload Scanner, which can happen. More on this later.
This section deals with bugs in WebKit and Blink’s Preload Scanner, and an
inefficiency in Firefox’s and IE/Edge’s Preload Scanner.
Firefox and IE/Edge: Place
@import before JS and CSS in HTML
In Firefox and IE/Edge, the Preload Scanner doesn’t seem to pick up any
@imports that are defined after
<script src="http://csswizardry.com/"> or
That means that this HTML:
<script src="http://csswizardry.com/app.js"></script> <style> @import url(app.css); </style>
…will yield this waterfall:
Here we can clearly see that the
@imported stylesheet does not start
<link rel="stylesheet" href="style.css" /> <style> @import url(app.css); </style>
The immediate solution to this problem is to swap the
<link and the
<style> blocks around. However, this will likely
break things as we change our dependency order (think cascade).
The preferred solution to this problem is to avoid the
@import altogether and
use a second
<link rel="stylesheet" />:
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="app.css" />
Blink and WebKit: Wrap
@import URLs in Quotes in HTML
WebKit and Blink will behave the exact same as Firefox and IE/Edge only if
@import URLs are missing quote marks (
"). This means that the Preload
Scanner in WebKit and Blink has a bug.
Simply wrapping the
@import in quotes will fix the problem and you don’t need
to reorder anything. Still, as before, my recommendation here is to avoid the
@import entirely and instead opt for a second
<link rel="stylesheet" />.
<link rel="stylesheet" href="style.css" /> <style> @import url(app.css); </style>
<link rel="stylesheet" href="style.css" /> <style> @import url("app.css"); </style>
This is definitely a bug in WebKit/Blink—missing quotes shouldn’t hide the
@imported stylesheet from the Preload Scanner.
Huge thanks to Yoav for helping me track this
<link rel="stylesheet" /> Before Async Snippets
The previous section looked at how CSS can be slowed down by other resources
(due to quirks, admittedly), and this section will look at how CSS can
inserted with an asynchronous loading snippet like so:
<script> var script = document.createElement('script'); script.src = "http://csswizardry.com/analytics.js"; document.getElementsByTagName('head').appendChild(script); </script>
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
A browser will not execute a
<script> if there is any currently-in flight
<link rel="stylesheet" href="slow-loading-stylesheet.css" /> <script> console.log("I will not run until slow-loading-stylesheet.css is downloaded."); </script>
This is by design. This is on purpose. Any synchronous
<script>s in your HTML
will not execute while any CSS is currently being downloaded. This is a simple,
defensive strategy to solve the edge case that the
<script> might ask
something about the page’s styles: if the script asks about the page’s
gives us could potentially be incorrect or stale. To mitigate this, the browser
doesn’t execute the
<script> until the CSSOM is constructed.
The upshot of this is that any delays on CSS’ download time will have a knock-on
impact on things like your async snippets. This is best illustrated with an
If we drop a
<link rel="stylesheet" /> in front of our async snippet, it will
not run until that CSS file has been downloaded and parsed. This means our CSS
is pushing everything back:
<link rel="stylesheet" href="app.css" /> <script> var script = document.createElement('script'); script.src = "http://csswizardry.com/analytics.js"; document.getElementsByTagName('head').appendChild(script); </script>
begin downloading until the moment the CSSOM is constructed. We’ve completely
lost any parallelisation:
Interestingly, the Preload Scanner would like to have picked up the
analytics.js ahead of time, but we’ve inadvertently hidden it:
"http://csswizardry.com/analytics.js" is a string, and doesn’t become a tokenisable
<script> element exists in the DOM. This is the bit I meant earlier
when I said
It’s very common for third party vendors to provide async snippets like this to
more safely load their scripts. It’s also very common for developers to be
suspicious of these third parties and place their async snippets later in the
page. While this is done with the best of intentions—
I don’t want to put—it can often be a net loss. In
<script>s before my own assets!
fact, Google Analytics even tell us what to do, and they’re right:
Copy and paste this code as the first item into the
<HEAD>of every webpage
you want to track.
So my advice here is:
<script>…</script> blocks have no dependency on CSS, place them
above your stylesheets.
Here’s what happens when we move to this pattern:
<script> var script = document.createElement('script'); script.src = "http://csswizardry.com/analytics.js"; document.getElementsByTagName('head').appendChild(script); </script> <link rel="stylesheet" href="app.css" />
Now you can see that we’ve completely regained parallelisation and the page has
loaded almost 2× faster.
Dang. This article is getting way, way more forensic than I intended.
Taking this even further, and looking beyond just async loading snippets, how
myself the following question and worked back from there:
- 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?
If the files do not depend on one another, then you should place your blocking
(The Preload Scanner ensures that, even though DOM construction is
blocked on the scripts, the CSS is still downloaded in parallel.)
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
doesn’t actually execute until the CSS if finished.
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
<link rel="stylesheet" /> 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
<html> <head> <link rel="stylesheet" href="app.css" /> </head> <body> <header class="site-header"> <nav class="site-nav">...</nav> </header> <main class="content"> <section class="content-primary"> <h1>...</h1> <div class="date-picker">...</div> </section> <aside class="content-secondary"> <div class="ads">...</div> </aside> </main> <footer class="site-footer"> </footer> </body>
This carries three key inefficiencies:
- Any given page will only use a small subset of styles found in
we’re almost definitely downloading more CSS than we need.
- 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
- The whole of
app.cssblocks 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):
<html> <head> <link rel="stylesheet" href="core.css" /> <link rel="stylesheet" href="site-header.css" /> <link rel="stylesheet" href="site-nav.css" /> <link rel="stylesheet" href="content.css" /> <link rel="stylesheet" href="content-primary.css" /> <link rel="stylesheet" href="date-picker.css" /> <link rel="stylesheet" href="content-secondary.css" /> <link rel="stylesheet" href="ads.css" /> <link rel="stylesheet" href="site-footer.css" /> </head> <body> <header class="site-header"> <nav class="site-nav">...</nav> </header> <main class="content"> <section class="content-primary"> <h1>...</h1> <div class="date-picker">...</div> </section> <aside class="content-secondary"> <div class="ads">...</div> </aside> </main> <footer class="site-footer"> </footer> </body>
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
page-footer.css takes a long time to download, the browser
can’t make a start on rendering
However, due to a recent change in Chrome (version 69, I believe), and behaviour
already present in Firefox and IE/Edge,
<link rel="stylesheet" />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:
<html> <head> <link rel="stylesheet" href="core.css" /> </head> <body> <link rel="stylesheet" href="site-header.css" /> <header class="site-header"> <link rel="stylesheet" href="site-nav.css" /> <nav class="site-nav">...</nav> </header> <link rel="stylesheet" href="content.css" /> <main class="content"> <link rel="stylesheet" href="content-primary.css" /> <section class="content-primary"> <h1>...</h1> <link rel="stylesheet" href="date-picker.css" /> <div class="date-picker">...</div> </section> <link rel="stylesheet" href="content-secondary.css" /> <aside class="content-secondary"> <link rel="stylesheet" href="ads.css" /> <div class="ads">...</div> </aside> </main> <link rel="stylesheet" href="site-footer.css" /> <footer class="site-footer"> </footer> </body>
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.
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.
- In your HTML;
- but in CSS especially;
- and beware of oddities with the Preload Scanner.
- but if it does depend on your CSS:
- Load CSS as the DOM needs it:
- This unblocks Start Render and allows progressive rendering.
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
Did you enjoy this? Hire me!