Modern Asynchronous CSS Loading

Posted by Scott on 11/30/2017

The simplest way to load a CSS file in an HTML document is to use a link element with rel="stylesheet":

<link rel="stylesheet" href="mycssfile.css">

Referencing CSS this way works great, but it comes with a downside: it's synchronous. In other words, with a typical stylesheet link like this, the browser stops rendering subsequent portions of the page while it requests, downloads, and parses the file. Sometimes that blocking can be desirable because we don't want the browser to render the page before it has the CSS it needs. But not all of our CSS files are critical enough to delay access to the content, which is why we highly recommend prioritizing and streamlining assets for fast, resilient delivery.

To load less-critical CSS files without blocking page rendering, we need to load them asynchronously.

Async CSS Loading Approaches

Historically, there are several ways to make a browser load CSS asynchronously, though none are quite as simple as you might expect.

One way (which works in modern browsers, at least) is to use JavaScript to create and insert a stylesheet link into the DOM:

// make a stylesheet link
var myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// insert it at the end of the head in a legacy-friendly manner
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

That last line's a doozy!

Another way is to set a stylesheet link's media attribute to a media type (or query) that does not match the user's current browsing environment, say perhaps media="print", or even something unrecognizable like media="nope!". Browsers will consider stylesheets for inapplicable media to be low-priority, and download them without blocking page rendering. That's good to know, but in order to display the stylesheet's rules once it loads, we need to use a JavaScript onload event handler to toggle the media value to something that matches the user's browsing environment, like screen or all:

<link rel="stylesheet" href="mystyles.css" media="nope!" onload="this.media='all'">

As a side note: we use a combination of the tricks above internally in our loadCSS.js library to handle asynchronous CSS loading, along with additional workarounds for old/still-active browsers that don't support onload events on link elements.

Similar to the media toggle approach, we could also load CSS asynchronously by marking a link as an alternate stylesheet (designed to offer the user alternate presentations of a site), and then using JavaScript to toggle the rel attribute back to stylesheet when the file loads:

<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">

The methods above do the job, but they share an unfortunate quality: they rely on JavaScript (not to mention knowledge of nuanced browser parsing behavior) to indirectly achieve the desired effect...

A Modern Approach

Thankfully, there's now a web standard that is designed specifically for loading resources like CSS asynchronously: rel="preload". Finally, we can load asynchronous CSS without JavaScript workarounds! Just kidding; perhaps surprisingly, even this method requires an onload event handler to make it work as we need. Still, it's our best option.

Here's an example of how we can use rel="preload" to load and apply a CSS file asynchronously (in a browser that supports it):

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">

Much like other attribute-toggling approaches above, rel="preload" will cause supporting browsers to download–but not apply–the referenced file, so again we need an onload handler to set the rel attribute to stylesheet once it finishes loading. This may not look like a big improvement over other approaches, but one advantage that rel="preload" brings is that supporting browsers will start downloading the file earlier than they would with say, a stylesheet with a non-matching media value.

Using rel=preload Today with loadCSS

Browser support for rel="preload" is, well... I mean, at least Chrome supports it. Other major browsers have committed support too, though. In fact, Firefox 56 already did support rel="preload", but its implementation was buggy enough (preload only worked for files that were explicitly deemed cacheable) that Firefox 57 shipped with the feature disabled (support will return again in Firefox 59).

Fortunately, we can test and polyfill support for rel="preload" to make sure it works everywhere. Our loadCSS project offers a script called cssrelpreload.js, which makes rel="preload" work for CSS files in browsers that don't support it natively (and it knows to do nothing if the browser knows how to handle preload on its own).

You can find step-by-step usage instructions for using cssrelpreload.js in the project readme, but in summary, including the script in your page either inline (or with server-push it if you're able), will automatically ensure that any link[rel="preload"] elements will work as expected in unsupporting browsers.

Here's an example of how we use it, including a noscript fallback, just in case:

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */ (function(){ ... }());
</script>

NOTE: if you've used cssrelpreload.js before, the latest version (2.0.1) has some nice improvements, and it no longer depends on the loadCSS.js script, so you can stop including that file and save some precious kilobytes.

As with any of our open source projects, you can find loadCSS on Github and on NPM. If you run into any issues, have questions, or want to contribute, please let us know in the issue tracker.

Thanks!