The Simplest Way to Load CSS Asynchronously

One of the most impactful things we can do to improve page performance and resilience is to load CSS in a way that does not delay page rendering. That’s because by default, browsers will load external CSS synchronously—halting all page rendering while the CSS is downloaded and parsed—both of which incur potential delays. Of course, at least a portion of a site’s CSS should be loaded before the page should be allowed to start rendering, and to get that initial CSS to the browser immediately, we recommend inlining (or HTTP2 server-pushing) the CSS. For sites with a small amount of overall, that alone might be enough, but if the CSS is large (say, bigger than 15 to 20kb), it can help performance to split it up by priority. Once split, less-critical CSS should be loaded in the background—AKA asynchronously. In this post, I aim to describe up our preferred way to do that these days, which has actually been around for years.

There are several ways to make CSS load asynchronously, but none are as intuitive as you might expect. Unlike script elements, there is no async or defer attribute to simply apply to a link element, so for years now we’ve maintained the loadCSS project to make the process of loading async CSS a little easier. Recently though, browsers have standardized their CSS loading behavior, so a dedicated script like loadCSS to handle their minor differences is likely no longer necessary.

The code

Today, armed with a little knowledge of how the browser handles various link element attributes, we can achieve the effect of loading CSS asynchronously with a short line of HTML. Here it is, the simplest way to load a stylesheet asynchronously:

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

Breaking that down…

This line of HTML is concise, but it’s not very intuitive, so let’s break down what’s going on here.

To start, the link's media attribute is set to print. “Print” is a media type that says, “apply this stylesheet’s rules for print-based media,” or in other words, apply them when the user tries to print the page. Admittedly, we want our stylesheet to apply to all media (especially screens) and not just print, but by declaring a media type that doesn’t match the current environment, we can acheive an interesting and useful effect: the browser will load the stylesheet without delaying page rendering, asynchronously! That’s helpful, but it’s not all we want. We also want the CSS to actually apply to the screen environment once it loads. For that, we can use the onload attribute to set the link's media to all when it finishes loading.

Can’t rel=preload do this too?

Yes, similarly! In the past year or two, we’ve been using link[rel=preload] (instead of rel=stylesheet) to achieve a similar pattern as above (toggling the rel attribute once loaded instead of the media attribute, respectively). It still works fine to use that approach, however, there are a couple of drawbacks to consider when using preload. First, browser support for preload is still not great, so a polyfill (such as the one loadCSS provides) is necessary if you want to rely on it to fetch and apply a stylesheet across browsers. More importantly though, preload fetches files very early, at the highest priority, potentially deprioritizing other important downloads, and that may be higher priority than you actually need for non-critical CSS.

Fortunately, if you happen to want the high-priority fetch that rel=preload provides (in browsers that support it), you can combine it with the above pattern, like this:

<link rel="preload" href="/path/to/my.css" as="style">
<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

Given the simple and declarative nature of the code above, we’d choose that over a polyfill, so the print media toggle approach is our preference again now.

Why not use a faux media attribute?

Anyone who has followed our writing on this for the past several years might recall that we’ve used media attribute values like “only x” to achieve the same effect as “print” by providing a value that doesn’t match any environment, as x is a nonsense media type. When browsers encounter media types that don’t match, they currently treat them all the same - they load the file anyway. That said, some browser teams are starting to consider differentiating between non-matching media types and those that are not valid (or aren’t recognized by the browser at all), and potentially not requesting files that are linked using invalid media types. This would break a lot of existing CSS loading implementations, so it’s not likely, but for safety sake we recommend using a valid, non-matching type like print.

You may not need loadCSS…

We continue to maintain loadCSS and find it useful in some situations, particularly for programatically fetching a CSS file from JavaScript, like this: loadCSS("/path/to/my.css"). If you’re already using loadCSS or its rel=preload polyfill pattern, you don’t necessarily need to change anything. Internally, it uses the same mechanics described in this article.

More and more though, we’re finding that the simple HTML approach may be all you need. And simple is often best.

Thanks for reading!

Questions? Hit us up on Twitter!

All blog posts