Lazy-Loading CSS Without a JS Dependency: Example

Established approach

The CSS on this page is lazy-loaded so as to be non-blocking. This technique can be used to improve page performance, making only essential CSS "render-blocking" and loading non-essential CSS only after the rest of the page has been drawn.

An excellent approach to this is demonstrated by Scott Jehl: using e.g. <link rel="stylesheet" href="my.css" media="print" onload="this.media='all'"> to create a stylesheet that doesn't apply because of a media type, then using Javascript to switch the media type after page load. Scott also provides a more-heavyweight example for general-purpose CSS-via-JS loading.

However, this approach falls over if Javascript fails or is unavailable. It provides an incomplete attempt at progressive enhancement: CSS adds design, and Javascript adds behaviour, and I shouldn't have to rely on the latter in order to get the former!

Alternative approach

The approach demonstrated by this page is different. It:

  1. Adds the stylesheet within a <noscript> tag to help ensure that it gets loaded (in a fallback/"blocking" fashion) if Javascript is unavailable. This tag gets a special lazyload attribute, e.g.: <noscript lazyload><link rel="stylesheet" type="text/css" href="my.css"></noscript>
  2. Uses Javascript to re-render the contents of each <noscript lazyload> tag after the document has finished loading.
  3. Works with all modern browsers. Optionally provides a fallback for Internet Explorer all the way back to version 6.

Code (modern browsers only)

      function lazyLoadCSS(){
        [...document.querySelectorAll('noscript[lazyload]')].forEach(ns=>ns.outerHTML=ns.innerHTML);
      }
      (document.readyState != 'loading') ? lazyLoadCSS() : document.addEventListener('DOMContentLoaded', lazyLoadCSS);
    

Code (IE6+ and modern browsers)

(View source of this page to see a full example.)

      function lazyLoadCSS(){
        if( ( window.navigator.userAgent.indexOf('MSIE ') + window.navigator.userAgent.indexOf('Trident/') ) > 0 ) {
          // Internet Explorer doesn't allow <noscript> tags to be accessible via the DOM, so we have to re-inject manually
          var link = document.createElement('link');
          link.rel = 'stylesheet';
          link.type = 'text/css';
          link.href = 'my.css';
          document.head.appendChild(link);
        } else {
          // Non-IE browsers can find <noscript lazyload> elements in the DOM and re-inject their content
          var lazyNoScripts = document.querySelectorAll('noscript[lazyload]');
          for(var i = 0; i < lazyNoScripts.length; i++){
            lazyNoScripts[i].outerHTML=lazyNoScripts[i].innerHTML;
          }
        }
      }

      // Trigger lazyLoadCSS after the page has loaded
      if (document.readyState != 'loading') {
        lazyLoadCSS();
      } else if (document.addEventListener) {
        document.addEventListener('DOMContentLoaded', lazyLoadCSS);
      } else {
        // Internet Explorer <9 doesn't support document.addEventListener
        document.attachEvent('onreadystatechange', function() {
          if (document.readyState != 'loading') lazyLoadCSS();
        });
      }
    

Other considerations

Further reading

For more information/history/justification, see my blog post. For just the source code, see this gist.