Can Preload Cut the Mustard?

Here at FG, we’re always looking out for new ways to simplify and improve the way we build websites. Sometimes our experiments result in big changes to our workflow, but even when they don’t, they usually broaden our understanding of the tools we can use. This post falls somewhere in the middle of the two.

More than a few times in the past year, we’ve mentioned link[rel=preload], a new attribute that can be used on the HTML link element. “Preload” allows us to specify resources that should be fetched early and asynchronously to be subsequently used in the page. If you missed it, my recent post about [rel=preload] described how we use it to load parts of our CSS asynchronously, and how our loadCSS script makes that work in browsers that don’t yet support the feature.

Recently, an interesting bug report was filed in the loadCSS tracker. It said that in the Chrome browser, if a preload link’s media attribute does not match, Chrome will not fetch the file that it references. For example, given the following code, Chrome will not fetch mystyles.css at all if the browser is narrower than 800px, because the media does not match.

<link rel="preload" href="my-styles.css" as="style" media="(min-width: 800px)">

At first glance, I was pretty sure that this would be an issue related to Chrome itself and not loadCSS, because Chrome supports preload natively and loadCSS is written to exit quietly and do nothing in supporting browsers. But even if it was a Chrome issue, it still sounded fishy, because my understanding was that the browser will always fetch a stylesheet file even its media doesn’t match—and that behavior’s been useful for years, for loading a “print” stylesheet for example. So I did some testing.

As it turns out, the report was valid: Chrome will not fetch that stylesheet in those conditions. That got me thinking: is this expected or is it a bug? The preload spec was unfortunately not clear on this, so I filed a bug in Chrome’s tracker and then took to the Twitters to ask around.

As it turns out, the behavior is expected, and I was happily surprised! I’ll explain why in a minute…

The Conditional Fetch

Permalink to 'The Conditional Fetch'

One of the primary patterns in building resilient websites is to use feature tests to make decisions about how to enhance the user interface in each browser. For a number of years, we’ve employed feature tests to qualify the way we load and/or apply enhancements to the page, and most recently we’ve standardized on an approach dubbed "Cutting the Mustard, " which goes like this:

  1. Perform a quick diagnostic test to see if the browser supports a few “modern” features
  2. If so, proceed to make broad modern enhancements to the page

…and in code, that could look like this, using our trusty old loadJS script to perform the fetch:

if( document.querySelector && localStorage ){
  // load a pile of JavaScript
  loadJS( "my-enhancements.js" );
}

…or even, perhaps like this, to load a particular JS file in only wider viewports:

if( matchMedia( "(min-width: 800px)" ).matches ){
  // load a pile of JavaScript
  loadJS( "my-enhancements.js" );
}

This approach works well, but from a maintenance perspective, it’s a little obtuse, and from a performance perspective, it requires parsing and running JavaScript before any request can be made. A declarative HTML approach would certainly be clearer, but the usual HTML methods of loading a script have never offered a means of qualifying whether a script should be fetched or not.

Could link[rel=preload] provide a better alternative? Maybe!

Cutting the Mustard with Preload

Permalink to 'Cutting the Mustard with Preload'

link[rel=preload] is designed to load many different types of files, not just CSS, so we can use it to reference the JavaScript file that we’d like to conditionally load. As we found above in the situation with the CSS file, we can qualify the conditions in which the file should be loaded by using the link’s’ media attribute. Admittedly, the media attribute is not nearly as flexible as JavaScript is for feature testing, but it does allow us to test CSS media queries, at least.

So here is how we can use preload to start to reproduce the second code example above:

<link rel="preload" href="my-enhancements.js" as="script" media="(min-width: 800px)">

Given the HTML above, a supporting browser will fetch my-enhancements.js only if the viewport is wider than 800px. Neat! Unfortunately, link[preload] only fetches a file, but does not evaluate it or apply it to the page. It just puts it into a local cache, ready for later use. To apply the file to the page, we need an onload event handler, and since we’re loading a JavaScript file, we’ll need to create a script element that we can use to execute the script once it loads. Here’s how that’d look…


<script>
function applyScript(src){
  var js = document.createElement( "script" );
  js.src = src;
  document.head.insertBefore( js, document.head.firstChild );
}
</script>
<link rel="preload" href="test.js" as="script" media="(min-width: 800px)" onload="applyScript(this.href)">

And here’s a demo page for the code above. It’ll load and evaluate the script in browsers that support preload, if the viewport is wider than 800px (You’ll know when the script loads because it will throw an alert message): Preload Cut the Mustard Demo 1

Worth it?

Permalink to 'Worth it?'

I think it’s unfortunate that link[rel=preload] offers no declarative means of evaluating a JavaScript file (or CSS either for that matter) on arrival, because we’re now back to using custom JavaScript to apply the file to the page. Still, there’s still an advantage here worth considering: link[rel=preload] will start to download the file as early as possible in the page loading process (unlike fetching the file via scripting alone), and that could make for a slightly more efficient process of enhancing a page.

That said, while we can indeed “cut the mustard” using link[rel=preload], browser support for “preload” is still not very broad. If we wanted to use it to qualify our script loading across all browsers, we’d need to test for support and provide a fallback… and that means a little more JavaScript. We’re in the weeds already, so let’s consider how that code might look.


<script>
  // preload support test
  var preloadSupported = (function() {
    try {
      return document.createElement("link").relList.supports("preload");
    } catch (e) {
      return false;
    }
  })();
  // apply on load
  function applyScript(src) {
    var js = document.createElement("script");
    js.src = src;
    document.head.insertBefore(js, document.head.firstChild);
  }
  // polyfill
  if (!preloadSupported) {
    var links = document.getElementsByTagName("link");
    for (var i = 0; i < links.length; i++) {
      var link = links[i];
      // qualify links to those with rel=preload and as=style attrs
      if (link.rel === "preload" && link.getAttribute("as") === "script" && !link.getAttribute("data-loaded")) {
        // prevent rerunning on link
        link.setAttribute("data-loaded", true);
        // if no media specified or media does match
        if (!link.media || matchMedia(link.media).matches) {
          applyScript(link.href);
        }
      }
    }
  }
</script>
<link rel="preload" href="test.js" as="script" media="(min-width: 800px)" onload="applyScript(this.href)">

…does anyone else smell burning rubber?

The polyfill adds an unfortunate amount of JavaScript, but as a proof of concept, it certainly seems to work! Here’s a demo page for the code above. It should load and run the script in most any browser, as long as the viewport is wider than 800px: Preload Cut the Mustard Demo 2

So it’s possible, at least! And it’s useful to know that we can use link[rel=preload] with qualifiers, even if the amount of code involved means we may not choose to use this in any production setting soon. Perhaps some tests would show that the early-preload request makes it worthwhile though, and in the future, polyfilling the preload behavior will become less necessary too.

Until then, we’ll likely keep cutting our mustard with an old but trusty knife.

All blog posts