• Sage

Asynchronous CSS Loading in Sage

Why load CSS asynchronously?

All stylesheets are render-blocking by nature. Meaning that referencing CSS stylesheets with <link rel="stylesheet"> causes the browser to stop parsing the HTML and wait while a stylesheet loads. This is bad for performance and will trigger warnings on all page speed tests.

Asynchronous CSS automation

The new <link rel="preload"> attribute allows us to load stylesheets asynchronously, without render-blocking. But preload only loads styles so we’ll also need to use onload to apply stylesheets once they’re loaded. And for browsers that don’t support preload, we are going to use loadCSS polyfill.

The tricky part for WordPress is that we can’t just edit <link> tags inside a template because we’re enqueuing our assets. So we need to hook into WordPress style_loader_tag and change the HTML output of <link> tags.

Edit <link> tags

Here is the code snippet that you’re going to need:

/**
 * Async load CSS
 */
if (env('WP_ENV') === 'production') {
    add_filter('style_loader_tag', function ($html, $handle, $href) {
        if (is_admin()) {
            return $html;
        }

        $dom = new \DOMDocument();
        $dom->loadHTML($html);
        $tag = $dom->getElementById($handle . '-css');
        $tag->setAttribute('rel', 'preload');
        $tag->setAttribute('as', 'style');
        $tag->setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
        $tag->removeAttribute('type');
        $html = $dom->saveHTML($tag);

        return $html;
    }, 999, 3);
}

Notice I’m using Bedrock env function to get WP_ENV variable and run this only in production. But it’s not a requirement so you can exclude this part or adjust for your setup.

Polyfill

Now we need to include the polyfill for older browsers. Let’s install an npm package to grab it. Run from within your theme:

yarn add fg-loadcss -D

Since it’s a small script it’s best to inline it in the <head> so it can async load CSS as soon as possible. For that, we’re going to output it in a separate JS file which we’ll later use to get the contents from and print in the head with the action hook.

Create a new file cssrelpreload.js inside Sage resources/assets/scripts/ folder and include this line to grab a polyfill from loadCSS:

import("fg-loadcss/dist/cssrelpreload.min");

There is a thing with Webpack that after it’ll copy this script to dist/ it will add additional code and since we want to inline the code, it doesn’t make sense and we want to keep it as light as possible. So if you’re using VS Code you can quickly open the script by holding ctrl and clicking on it, then copy the contents to your file.

Finally, let’s echo it inside the <head> tag.

if (env('WP_ENV') === 'production') {
    add_action('wp_head', function () {
        $preload_script = get_theme_file_path() . '/resources/assets/scripts/cssrelpreload.js';

        if (fopen($preload_script, 'r')) {
            echo '<script>' . file_get_contents($preload_script) . '</script>';
        }
    }, 101);
}

Critical CSS

Now that we’re loading all of our stylesheets asynchronously a page will load with plain HTML and only when the styles are fully loaded they’re going to be applied and force layout and paint.

This produces an undesirable flash of unstyled content (FOUC) and it doesn’t look good. So obviously, we would like to avoid that. To fix it, we need to inline a small portion of CSS that’s critical for above the fold content.

You could try to create a separate stylesheet with your critical CSS manually to keep a tight grip on what styles should be inlined but it’s a tedious process and it becomes even worse for projects that need maintenance. So it’s way better to automate it to save time. We’ll use the Webpack plugin.

Run:

yarn add html-critical-webpack-plugin@1.1.0 -D

Note that version here is important if you’re using Sage 9 with Webpack 3 which doesn’t support the latest version of this plugin.

Inside webpack.config.optimize.js include:

...
const HtmlCriticalWebpackPlugin = require("html-critical-webpack-plugin");

module.exports = {
  plugins: [
    ...
    new HtmlCriticalWebpackPlugin({
      base: config.paths.dist,
      src: config.devUrl,
      dest: "styles/critical-home.css",
      ignore: ["@font-face", /url\(/],
      inline: false,
      minify: true,
      extract: false,
      dimensions: [
        {
          height: 375,
          width: 565,
        },
        {
          height: 1080,
          width: 1920,
        },
      ],
      penthouse: {
        blockJSRequests: false,
      },
    }),
  ]
...
};

Shout out to Roots Discourse member LucasDemea for this configuration snippet that uses Sage config values.

You can notice in here that I’m excluding @font-face rules and background URLs as these are not critical for page load but can significantly bloat your inline CSS. Also, you can adjust dimensions to reflect your site’s media breakpoints.

This plugin uses critical package as a dependency so you can find all configuration options in there.

Most websites will have a few different layouts so you’ll probably want to generate critical CSS for each one of them. Simply copy-pasting this plugin call and changing dest value will do the job. But anyone who knows a DRY solution for this is welcome to chime in on Discourse.

The last thing to do is to inline extracted CSS in the <head> as early as possible.

/**
 * Inject critical assets in head as early as possible
 */
if (env('WP_ENV') === 'production') {
    add_action('wp_head', function () {
        if (is_front_page()) {
            $critical_CSS = asset_path('styles/critical-home.css');
        } elseif (is_singular()) {
            $critical_CSS = asset_path('styles/critical-singular.css');
        } else {
            $critical_CSS = asset_path('styles/critical-archive.css');
        }

        if (fopen($critical_CSS, 'r')) {
            echo '<style>' . file_get_contents($critical_CSS) . '</style>';
        }
    }, 1);
}

Wrapping up

That’s it! You have set up an automated process for loading CSS asynchronously, generating and inlining critical CSS.

Join the discussion on Roots Discourse

Join over 5,800 subscribers on our newsletter to get the latest Roots updates, along with occasional tips on building better WordPress sites.

Looking for WordPress plugin recommendations, the newest modern WordPress projects, and general web development tips and articles?

“Easily the best WordPress email I get.” Colin OBrien

Follow us on Twitter @rootswp

Ready to checkout?