Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Preloading with Critical Path CSS

The link rel="preload" resource hint combined with defining your critical path CSS may provide a better solution to deferring the loading of your CSS files. This feature is still very much new, and is currently only supported by the latest versions of Chrome and Opera.

What does Preloading do?

There are a variety of new resource hints being drafted by the W3C, including prefetch and preload. While these are similar, we will see that there is a big difference.

So, what does preloading do?

From the W3C abstract:

This specification defines the preload keyword that may be used with link elements. This keyword provides a declarative fetch primitive that initiates an early fetch and separates fetching from resource execution.

In other words, it tells the browser that the resource must be fetched, as soon as possible, and outside the context of fetching other resources. The current solution to optimize the delivery of your CSS is to:

  1. Inline or define your critical path CSS with your html response. This enables the browser to render your above-the-fold content almost instantly.
  2. Reduce blocking script and stylesheets. A common solution to this is to load your CSS stylesheets via XHR after the page has rendered.

While you still will want to perform step 1, the link rel="preload" solves step 2.

Preloading in Action

It’s simple - just use a rel=“preload” for your stylesheet’s <link> tag instead of rel="stylesheet". That will instruct the browser to fetch the resource. However, the CSS that is fetched is not processed. To do that, simply add an onload event handler. This event is fired when the resource has been fetched. In the event handler code set the rel property for the <link> element back to “stylesheet”. We’ll also add a <noscript> tag for fallback support for browsers that are blocking scripts. Here’s what is looks like:

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

Fallback Support

You can roll your own code for browser’s that do not support the preload yet, or you can use the popular loadCss library. The library will detect if your browser supports the preload resource hint, and if not, will fall back to injecting link[rel=stylesheet] elements into to DOM after the last link or script tag. To implement this we need to:

  1. Define our critical path CSS inline or in the head element
  2. Include the link[rel=preload] elements along with the <noscript> fallback
  3. Inline the contents of the loadCss.js file
  4. Then, inline the contents of the cssrelpreload.js file

Here’s a quick outline of using the loadCss approach:

<html>
  <head>
    <style>
      /* Critical path CSS */
    </style>

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

    <script>
      /*loadCss*/
      !(function (w) {
        'use strict';
        var loadCSS = function (href, before, media) {
          function ready(cb) {
            return doc.body
              ? cb()
              : void setTimeout(function () {
                  ready(cb);
                });
          }
          function loadCB() {
            ss.addEventListener && ss.removeEventListener('load', loadCB),
              (ss.media = media || 'all');
          }
          var ref,
            doc = w.document,
            ss = doc.createElement('link');
          if (before) ref = before;
          else {
            var refs = (doc.body || doc.getElementsByTagName('head')[0])
              .childNodes;
            ref = refs[refs.length - 1];
          }
          var sheets = doc.styleSheets;
          (ss.rel = 'stylesheet'),
            (ss.href = href),
            (ss.media = 'only x'),
            ready(function () {
              ref.parentNode.insertBefore(ss, before ? ref : ref.nextSibling);
            });
          var onloadcssdefined = function (cb) {
            for (var resolvedHref = ss.href, i = sheets.length; i--; )
              if (sheets[i].href === resolvedHref) return cb();
            setTimeout(function () {
              onloadcssdefined(cb);
            });
          };
          return (
            ss.addEventListener && ss.addEventListener('load', loadCB),
            (ss.onloadcssdefined = onloadcssdefined),
            onloadcssdefined(loadCB),
            ss
          );
        };
        'undefined' != typeof exports
          ? (exports.loadCSS = loadCSS)
          : (w.loadCSS = loadCSS);
      })('undefined' != typeof global ? global : this);

      /*link[rel=preload] polyfill*/
      !(function (w) {
        if (w.loadCSS) {
          var rp = (loadCSS.relpreload = {});
          if (
            ((rp.support = function () {
              try {
                return w.document
                  .createElement('link')
                  .relList.supports('preload');
              } catch (e) {
                return !1;
              }
            }),
            (rp.poly = function () {
              for (
                var links = w.document.getElementsByTagName('link'), i = 0;
                i < links.length;
                i++
              ) {
                var link = links[i];
                'preload' === link.rel &&
                  'style' === link.getAttribute('as') &&
                  (w.loadCSS(link.href, link), (link.rel = null));
              }
            }),
            !rp.support())
          ) {
            rp.poly();
            var run = w.setInterval(rp.poly, 300);
            w.addEventListener &&
              w.addEventListener('load', function () {
                w.clearInterval(run);
              }),
              w.attachEvent &&
                w.attachEvent('onload', function () {
                  w.clearInterval(run);
                });
          }
        }
      })(this);
    </script>
  </head>
  <body></body>
</html>

Just to note: I ran the loadCss.js and cssrelpreload.js files through uglify-js to compress the contents. I would recommend that you grab the latest version of the code from GitHub and compress it yourself rather than copying & pasting from above.

Preload vs Prefetch

Lastly, there is also a resource hint attribute prefetch. This is more widely supported by modern browsers, but it much different than preload. First, the resource is only fetched if-and-when the browser is idle. So, this is not a good solution for our application’s stylesheet(s), unless you are fetching stylesheets that may be needed in your application later.

\n \n \n\n```\n\nJust to note: I ran the loadCss.js and cssrelpreload.js files through uglify-js to compress the contents.\nI would recommend that you grab the latest version of the code from GitHub and compress it yourself rather than copying & pasting from above.\n\n## Preload vs Prefetch\n\nLastly, there is also a resource hint attribute `prefetch`.\nThis is more widely supported by modern browsers, but it much different than `preload`.\nFirst, the resource is only fetched if-and-when the browser is idle.\nSo, this is not a good solution for our application's stylesheet(s), unless you are fetching stylesheets that _may_ be needed in your application later.\n","author":{"@type":"Person","image":"/headshots/brian-love-1.jpg","name":"Brian Love","sameAs":"https://brianflove.com/"},"datePublished":"2016-07-21T00:00:00.000Z","description":"The `link rel=\"preload\"` resource hint combined with defining your critical path CSS may provide a better solution to deferring the loading of your CSS files.","headline":"Preloading with Critical Path CSS","name":"Preloading with Critical Path CSS","url":"https://brianflove.com"}