Create a gist now

Instantly share code, notes, and snippets.

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.

tkadlec commented Jun 10, 2014

I ran it through a few tests as well and I'm seeing the same basic results as you, which makes sense in this case. And I really like the script version for the qualification benefits.

What happens if there is an inline block of CSS in the head as well? That would delay the script from executing at least briefly until the CSSOM is available. I'm guessing it would have only a slight impact, but I'd be curious to see it nonetheless.

Owner

scottjehl commented Jun 10, 2014

Thanks, @tkadlec. Good question. I think putting the script before the inline style element is a good way to avoid that delay you mentioned, right? That's what I've been doing lately, per Ilya's recommendation, and it seems to work.

tkadlec commented Jun 10, 2014

Ah, right! 👍

Cool. I'm going to personally run with this a bit on a few projects and see how it works out, but it sounds super solid. Yay!

Owner

scottjehl commented Jun 10, 2014

Fwiw, this is a simplified example but we tend to have a function in the head that can fetch a CSS or JavaScript file asynchronously.

We fetch the CSS usually without much qualification (or maybe after testing something simple like matchMedia support), and then we check some browser qualifications like querySelector or addEventListener support before fetching one 15-30kb "enhancements.js" file.

I did some work on this recently and found that B is not really asynchronous. When I added an artificial delay (something big like 2s) into the response for a CSS file referenced in a <link> it still blocked whilst the browser waited for the network response.

If your CSS is returning in a timely fashion it doesn't make much difference and to actually do this properly asynchronously you need to use an XMLHttpRequest and add the result to a <style> element so it's probably more hassle than it's worth.

Full write up of what I found is here: http://decadecity.net/blog/2014/05/07/asynchronous-css-delivery-for-fault-tolerance

i'm pretty sure that nothing happens until all css is loaded, correct me if i'm wrong, but adding any link elements outside of the head is going to affect load time. why not use link prefetch/prerender/etc? if the styles aren't critical? i use b) solution often, and always for third party scripts and style sheets.

Owner

scottjehl commented Jun 10, 2014

That's pretty interesting, @decadecity. Thanks. I'll do some testing with a slow file to see if I can't reproduce that.

@scottjehl With pre-processors and async scripts things get murcky pretty quick so didn't fully and systematically test things with slow CSS but there were cases where it did block DOMContentLoaded: http://decadecity.net/blog/2014/05/07/learnings-from-researching-fault-tolerent-css-delivery#links-and-domcontentloaded

Adding a <link> with JS is probably good enough in the majority of cases.

Owner

scottjehl commented Jun 10, 2014

This approach from @gui-poa is pretty interesting.
I guess it piggybacks on the browser's native de-prioritization of non-screen media. It does lose the ability to qualify the request https://gist.github.com/gui-poa/8ba1315befeafc915189

Owner

scottjehl commented Jun 10, 2014

Not sure but I wonder if an alternate stylesheet would do a similar service without leaving a print stylesheet hanging when JS is unavailable.

Owner

scottjehl commented Jun 10, 2014

Minor update but after reading https://gist.github.com/igrigorik/2935269 I edited the script-loaded version slightly and uncommented the CSS so you can see it apply in a non-blocking manner.

The change to the script version is that the link is injected with a non-applicable media query, which means it won't block rendering. After append, the media type is set back to all.

cc @tkadlec

@scottjehl What do you think would qualify as "non-critical CSS"? Thanks!

Owner

scottjehl commented Jun 10, 2014

@howdyjames Good question. The advice tends to be vague, but the idea seems to be to defer any CSS that isn't necessary to render the initial page layout

adactio commented Jun 10, 2014

I feel a little squeamish about making CSS dependent on JavaScript.

How about loading in the CSS with JavaScript in the head, but then having the regular link wrapped in a noscript element at the end of the body?

(Normally I'd avoid noscript like the plague but here it feels like it could be fit for purpose.)

What do you think?

matijs commented Jun 10, 2014

@scottjehl Are you trying to (pre)cache css for subsequent pages? I'm struggling a little to see other use cases.

Owner

scottjehl commented Jun 10, 2014

@adactio Great point. I agree and added a note up top. I guess it also depends on how truly non-critical that second stylesheet is too. The experience could be fine without it, right?
Another approach could be to inline the small-screen styles and then reference the larger breakpoint styles via a link with a media attribute representing its minimum breakpoint. That'd at least allow the second request to load without blocking in places where the media query doesn't apply, but the implication that large screens will have a faster connection and won't mind that blocking request as much seems... debatable I guess.

@matijs inlining some of the CSS will lose out on regular caching in favor of having fewer obstacles in the critical path to a fast page load. More on that here https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent

@scottjehl some webpagetest runs would go a long way here :)

The "link at the bottom of the page" approach is not representative. I wouldn't use it as a concrete data point: the performance of that approach will vary a great deal based on size of the HTML document and browser-specific implementations (e.g. some browsers will yield an early paint if they consumed enough tokens / are blocked / are stuck waiting for a big HTML doc for a long time). I'd just avoid it entirely. If you're placing a <link> tag in your doc, put it at the top.

In the short term, script-injected + non-applicable MQ is probably the best approach. In the (hopefully not so distant) future, <link lazyload> should make this much simpler: https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/ResourcePriorities/Overview.html#the-link-element

Owner

scottjehl commented Jun 10, 2014

Thanks, Ilya!

@scottjehl Just a heads-up. I used to put a link rel="stylesheet" at the very bottom of my page but the browser prioritized it and it was requested before all the JS/fonts/images (which were all referenced higher in the page). I can post a network waterfall diagram to prove this tomorrow.

Owner

scottjehl commented Jun 11, 2014

Sounds about right, @simevidas. I think that's another good reason to avoid that approach for this use case.

phamann commented Jun 11, 2014

We've been using this approach for nearly a year now in production on the Guardian responsive site, and have learnt some of the issues mentioned in this thread along the way. Our current approach is as follows:

In the head of the document:

  • Inline critical CSS (placed as soon as possible within the head, even before most <meta>)
  • Sync load all global styles with a <link> for older browsers
  • Load global styles from localStorage if available and inject into <style> element

https://github.com/guardian/frontend/blob/master/common/app/views/fragments/commonCssSetup.scala.html

In the foot of the document:

  • Include no script for older browsers
  • Load async
    -- Inline into <style> in the head
    -- Cache into localStorage for next page load

https://github.com/guardian/frontend/blob/master/common/app/views/fragments/loadCss.scala.html

The reason we use localStorage is to be more resilient against single point of failures. I.e. we now have the entire styles of the page within the initial html payload (a single HTTP request). You could argue that the same could be said if the global styles are in the HTTP cache, but this is rarely the case on mobile. Hopefully the ServiceWorker spec will standardise such hacking and allow us to design for offline first.

With regards to the "what are critical styles" question, ours are a mixture of breakpoints to avoid FOUC on larger devices, but largely covering the header, navigation, base layout and content typography. Due to the file size restrictions (we enforce max 14kb gzipped), we've found that we can't have a single head file that accommodates every section/content type across the site. So have resorted to maintaining ~3-4 head files to fine tune the experience. This does result in duplicate styles in your global stylesheet, but the trade-off is worth it, you just have to be careful of the cascade.

I hoping that a combination of HTTP/2 and the Resource priorities API are going to make these techniques anti-patterns in the future 😉. Thanks for opening this thread @scottjehl it's very interesting to hear others experiments.

@simevidas That's my experience too, I tried putting a link at the end of the page but found it made no difference in the RUM data for time to parse the document and DOMContentLoded, even if the link was injected by JS. Making it genuinely async brought both those times down by ~0.5s.

@adactio I share your concerns about making CSS dependent on JS but, providing the page is usable with the inline CSS, when JS isn't available the extra styles are just another progressive enhancement.

In terms of splitting what CSS is inlined and what is lazy loaded my experience of this comes down to two main factors:

  1. Is the page usable with the inline styles? As stated above, the page has to at least function with JS disabled, even if it's not going to win any design awards in this state.
  2. How much of a FOUC is there when the enhanced styles load? On a slow network there can be a noticeable delay before the enhanced styles arrive and, if the page was already usable, then large jumps in content layout are off putting.

The browser's default behaviour of blocking as CSS loads is very safe and saves us having to think about some complicated things, by doing this we are taking responsibility for the consequences and have a new set of problems to solve.

Owner

scottjehl commented Jun 11, 2014

@phamann lots of interesting tips there, thanks!
Had you also found in your testing that the insertBefore + non-applicable media toggle is best for ensuring a non-blocking request request? With the media toggle removed I've seen the request end up blocking render, so I wondered if you're also doing that at the Guardian (I haven't looked at that part of your code yet). I need to go back and add that to our projects now!

Owner

scottjehl commented Jun 11, 2014

Update: loadCSS has its own home now! https://github.com/filamentgroup/loadCSS/

phamann commented Jun 11, 2014

@scottjehl no we had noticed that, very nice trick! I'll add that to our implementation now. Thanks.

Would it be better to place the <link media="only x"> elements directly in the head and let JS only change the @media attribute? The JS could be condensed to

setTimeout(function() {
  Array.prototype.slice.call(document.head.querySelectorAll('link[media="only x"]'))
    .forEach(function(link) {
      link.setAttribute('media', 'screen');
    });
});

(Newer browsers assumed.)

gui-poa commented Jun 11, 2014

Scott, in which browsers this technique (loadCSS function) works?

The reason we use localStorage is to be more resilient against single point of failures. I.e. we now have the entire styles of the page within the initial html payload (a single HTTP request). You could argue that the same could be said if the global styles are in the HTTP cache, but this is rarely the case on mobile.

@phamann have you measured this? I'd love to see some real data proving this .... and then figure out why, because this certainly shouldn't be the case.

shshaw commented Jun 11, 2014

Interesting idea. Since you recommend including the non-essential CSS in a <noscript> wrapper, why not use that <noscript> element as the CSS file list that loadCSS then pulls from?

Here's a very rough, very quick mockup in jQuery:

<noscript class="async-css">
  <link rel="stylesheet" href="http://scottjehl.com/test/loadingcss/foo.css" media="all">
</noscript>
<script>
  $("noscript.async-css").each(function(){
    var $this = $(this),
        $links = $($this.text()).filter("link")
          .attr("media","only foo")
          .appendTo($this.parent());

    setTimeout( function(){ $links.attr("media","all"); },1 );
  });
</script>

It would take a little adjusting to be made in pure Javascript, but I like having the fallback built-in and reducing the repetition of the CSS URLs.

shshaw commented Jun 11, 2014

Created a fork with a pure Javascript version of the functionality I describe above.

<noscript class="loadCSS"><link rel="stylesheet" href="myStyles.css" media="all"></noscript>
<script>
loadCSS();
</script>

Here's a JSBin example as well: http://jsbin.com/vazon/3/edit

Only tested in Chrome, but it should run fine cross-browser.

For what it's worth I was working on something similar the other day. I modified the script on Google Developers so that I could pass in a string (in my case the classes applied to the body tag), and then not only load multiple CSS files asynchronously, but also filter them by looking for a match in that string - basically you can conditionally load CSS files based on 'pagetype', meaning you can cut back even more on loading styles that aren't required for the particular page you're on.

Here's my script if you're interested: https://gist.github.com/matt-bailey/602b40c77a5d3381ff26

I like what you did with 'media' in order to ensure you can fetch without blocking the render. I'm going to incorporate that in my version if that's ok?

@matt-bailey - wouldn't that require or assume that the site is styled page by page - something that most devs now a days would try hard to avoid?

shshaw commented Jun 12, 2014

@woeldiche If only certain pages have a module with particularly heavy styles, why not only include those styles on relevant pages? I try to avoid that in general, but if you're using a CMS and plugins, you may not be the author of the styles. Keeping overall page weight down by only including that when necessary seems like a win to me.

@shshaw True. Loading stylesheets based on modules would make good sense to me. A lot of projects are already structured that way as sass/less partials.

My concern would be the number of request unless you also utilize a module bundler to combine a number of css modules into a low number of requests. That again would of course be a trade-off between caching and http-request.

It should be possible to combine the above technique with a bundler like http://webpack.github.io/ for css modules.

This is great, I've been looking for a better approach to handle non critical css! Will put to use.

@phamann: Do you then classify all browsers that don't support localStorage as old? Otherwise they will have to reload all CSS inline on every page?

Regarding critical styles, I include everything that's above the fold on the current page. I built a tool that can automatically generate this CSS for you (you can use it in your build).

The idea of having more than two levels of importance for the CSS to load is interesting... Obviously CSS related to different pages goes on the non-important pile.. But say icon-fonts, dynamically inserted content, content just below the fold... could certainly make for a second-teir "critical" CSS.. But how would this be used? Injected into the DOM below the corresponding (arbitrary) fold? Not sure if it's worthwhile introducing this level of complexity.

@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! :)

@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.

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 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.

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 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.

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

@ghost

ghost commented May 26, 2016 edited by ghost

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


jhabdas 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