Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Comparing two ways to load non-critical CSS

I wanted to figure out the fastest way to load non-critical CSS so that the impact on initial page drawing is minimal.

TL;DR: Here's the solution I ended up with: https://github.com/filamentgroup/loadCSS/


For async JavaScript file requests, we have the async attribute to make this easy, but CSS file requests have no similar standard mechanism (at least, none that will still apply the CSS after loading - here are some async CSS loading conditions that do apply when CSS is inapplicable to media: https://gist.github.com/igrigorik/2935269#file-notes-md ).

Seems there are a couple ways to load and apply a CSS file in a non-blocking manner:

  • A) Use an ordinary link element to reference the stylesheet, and place it at the end of the HTML document, after all the content. (This is what Google recommends here https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent )
  • B) Fetch it asynchronously with some inline JavaScript from the head of the page, by appending a link element to page dynamically. (To ensure it's async, I'm setting the link's media to a non-applicable media query, then toggling it back after the request goes out).

I suspected B would initiate the request sooner, since A would be dependent on the size of the content in the document - parsing it in entireity before kicking off the request.

In this case, sooner would be ideal to reduce impact on our page load process, since there could be minor reflows that are triggered by the styles in the non-critical CSS file (ideally, the critical CSS would have all reflow-triggering styles in it, but that so far, that's been hard to pull off across the breakpoints of a responsive design, and given that the initial inlined CSS should be kept very small to fit into the first round trip from the server).

I made some test pages, and they do seem to confirm that B will load the CSS sooner (B requests the css file after around 60-70ms whereas A usually requests it around 130-200ms). It's consistently about twice as fast.

B comes with the additional benefits of qualifying the request based on whatever conditions we care to test too, which is nice.

@matt-bailey
Copy link

matt-bailey commented Jun 20, 2014

@woeldiche, @shshaw All our projects use a very modular, component based Sass structure, from which we are able to build a critical.css file (inlined in the head), a base.css file (global styles) and then, if required, the styles for particular page types.

For example, we do a lot of ecommerce sites - if we know that a specific component is only used on Product Pages then it goes in product-page.css.

Yes this adds one more http request, but the files are loaded asynchronously and once the browser has that file it's cached. Plus this also massively reduces redundancy - I tested one of our, non-optimised sites the other day and 85% of the styles in main.css weren't being used.

Of course these CSS loading techniques aren't the only facet involved in optimising the critical rendering path, but we've seen our optimised sites conversion rates shoot up after implementation - making for very happy clients! :)

@kukulich
Copy link

kukulich commented Aug 30, 2014

@scottjehl Does this really work? I have just tried last versions of Firefox and Chrome and both loaded my CSS synchronously and blocked rendering. I tested it with one very slowly loading CSS file so I can really see if it works.

@thienedits
Copy link

thienedits commented Oct 27, 2014

Here's a method I found using XHR. Only drawback is it injects the css styles into a style element.

https://gist.github.com/thienedits/d99aa3f841a1a156d47a

@Jakobud
Copy link

Jakobud commented Mar 11, 2015

@thienedits I don't think that's really a drawback. If something is sitting in a separate file or if it's injected into the document, your styling with behave the same way. Your browser will cache an injected stylesheet too. In fact, this is a common way of dealing with SVG files. You can keep your SVG cachable, and inject it's contents directly into your HTML during page load. This gives the advantage of caching but allows allows you to leverage every aspect of SVG's, such as dynamically changing SVG element styles, etc, which you cannot do when simply referencing an external SVG file.

@FibreFoX
Copy link

FibreFoX commented Mar 31, 2015

There is one problem with injected <style>: injecting CSS via inline-style-block could be blocked by CSP:
https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives#style-src

Despite of all existing (script-)workarounds for this problem, we have to finde a solution for the real problem: page-blocking css-requests! That lazyload-attribute looks like the solution for web-devs, you can influence the browser-behaviour, just like async on scripts. It's like a mysterious chicken-egg-problem to me, because using "progressive enhancement" seems not to work here (1. html, 2. style, 3. script), every part could be blocked (doesn't always have to be disabled by the user itself, some adblockers or anti-script-addons of browsers can make this fail), or just not fully parsed.

For me its a really interesting topic, which definately should have more eyes on it.

@2kool2
Copy link

2kool2 commented Jul 31, 2015

I'm just curious, has anyone tried downloading the CSS as a script with type="text/css" and async?

<script id="styles" async type="text/css" src="style.css"></script>
<noscript><link rel="stylesheet" href="style.css"></noscript>

Surely that'd be non-blocking?
Then use JS to either rename the script tag to style, or copy its content across.
I tried something similar for overwriting critical-path-only CSS, which worked but was unused for other reasons.

@Garconis
Copy link

Garconis commented Mar 17, 2016

For the record, Demo A loads the CSS faster than Demo B. For me, at least.

Copy link

ghost commented May 26, 2016

head

<script>
      (function() {
      'use strict';
      var head = document.getElementsByTagName('head')[0];
      var bootnap = document.createElement('link');
      bootnap.rel = 'stylesheet';
      bootnap.href = './css/bootstrap-theme1.min.css';
      head.appendChild(bootnap);
      }());
</script>

Before </body>

<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'>

Result:

1

score1


head

<script>
// notice the difference between this one and the first one
      (function() {
          'use strict';
          var head = document.getElementsByTagName('script')[0];
          var bootnap = document.createElement('link');
          bootnap.rel = 'stylesheet';
          bootnap.href = './css/bootstrap-theme1.min.css';
          bootnap.media = 'only x';
          head.parentNode.insertBefore(bootnap, head);
          setTimeout(function() {
            bootnap.media = 'all';
          });
      }());
</script

Before </body>

<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'>

Result:

2

score2


In the head

<script>
      (function() {
          'use strict';
          var head = document.getElementsByTagName('script')[0];
          var bootnap = document.createElement('link');
          bootnap.rel = 'stylesheet';
          bootnap.href = './css/bootstrap-theme1.min.css';
          bootnap.media = 'only x';
          head.parentNode.insertBefore(bootnap, head);
          setTimeout(function() {
            bootnap.media = 'all';
          });
      }());
</script>

Before </body>

<script>
      (function() {
          'use strict';
          var xhr = new XMLHttpRequest();
          xhr.timeout = 4000;
          xhr.overrideMimeType('text/css; charset=UTF-8');
          xhr.onreadystatechange = function() {
              if (xhr.readyState === 4 && xhr.status === 200) {
                  var style = document.createElement('style'),
                      lastJS = document.getElementsByTagName('script')[2];
                  style.appendChild(document.createTextNode(xhr.responseText));
                  lastJS.appendChild(style);
              }
          };
          xhr.open('GET', 'https://fonts.googleapis.com/css?family=Roboto:400,700', true);
          xhr.send(null);
      }());
</script>

Result:

3

score3


The sweet spot (inlined critical above-the-fold css into the page itself):

Before </body>

<script>
    (function() {
        'use strict';
        var getAsyncFile = function(fileStr) {
            var xhr = new XMLHttpRequest();
            xhr.timeout = 4000;
            xhr.overrideMimeType('text/css; charset=UTF-8');
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var style = document.createElement('style'),
                        head = document.getElementsByTagName('head')[0];
                    style.appendChild(document.createTextNode(xhr.responseText));
                    head.appendChild(style);
                }
            };
            xhr.open('GET', fileStr, true);
            xhr.send(null);
        };
        getAsyncFile('./css/bootstrap-theme1.min.css');
        getAsyncFile('https://fonts.googleapis.com/css?family=Roboto:400,700');
</script>

Result:

4

The Desktop score went from 94 to 96 using the last example.


The browser is now fetching the bootstrap theme at 180ms, instead 220ms:

In the head

<script>
    (function(w) {
        'use strict';
        var xhrRunner = {
            firstRun: true
        };
        xhrRunner.getAsyncFile = function(fileStr) {
            var xhr = new XMLHttpRequest();
            xhr.timeout = 4000;
            xhr.overrideMimeType('text/css; charset=UTF-8');
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var style = document.createElement('style'),
                        head = document.getElementsByTagName('head')[0];
                    style.appendChild(document.createTextNode(xhr.responseText));
                    head.appendChild(style);
                }
            };
            xhr.open('GET', fileStr, true);
            xhr.send(null);
        };
        if (xhrRunner.firstRun) {
            xhrRunner.getAsyncFile('./css/bootstrap-theme1.min.css');
            xhrRunner.firstRun = false;
        }
        w.xhrRunner = xhrRunner;
    }(window));
</script>

Before </body>

<script>
    (function() {
        'use strict';
        xhrRunner.getAsyncFile(
        'https://fonts.googleapis.com/css?family=Roboto:400,700'
        );
    }());
</script>

Result:

5


Four days later:

Replaced the Roboto fonts with Helvetica.

Made seperate page that includes all the classes that my blog is using and used grunt-uncss. It looks like I've been using only 10% (13KB out of 126KB) of the bootstrap framework. The 13kb css is inlined into the page itself. My blog engine is separated on two parts, blog-engine 7.8kb and post-engine 75.5kb, instead serving one gigantic 90kb


Copy link

ghost commented Mar 6, 2017

I've developed and open sourced a new approach to handling asynchronous CSS and JS so I could use PhotoSwipe on my new blog. I hope you like it: https://www.npmjs.com/package/fetch-inject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment