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.

Updated: 2019-09-10

There is a more simple way to load CSS asynchronously. link’s media attribute set to print has an interesting effect: the browser will load the stylesheet without render-blocking. Combined with onload attribute we can load CSS asynchronously and apply it with just a single line of code! Also, we get full browser support with no polyfills.

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
 */
add_filter('style_loader_tag', function (string $html, string $handle): string {
    if ('development' === env('WP_ENV') || is_admin()) {
        return $html;
    }

    $dom = new \DOMDocument();
    $dom->loadHTML($html);
    $tag = $dom->getElementById($handle . '-css');
    $tag->setAttribute('media', 'print');
    $tag->setAttribute('onload', "this.media='all'");
    $tag->removeAttribute('type');
    $tag->removeAttribute('id');
    $html = $dom->saveHTML($tag);

    return $html;
}, 999, 2);

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

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: [
        {
          width: 360,
          height: 640,
        },
        {
          width: 1920,
          height: 1080,
        },
      ],
      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 and src values 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
 */
add_action('wp_head', function (): void {
    if ('development' === env('WP_ENV')) {
        return;
    }

    if (is_front_page()) {
        // $critical_CSS = locate_asset('styles/critical-home.css');
        $critical_CSS = 'styles/critical-home.css';
    } elseif (is_singular()) {
        // $critical_CSS = locate_asset('styles/critical-singular.css');
        $critical_CSS = 'styles/critical-singular.css';
    } else {
        // $critical_CSS = locate_asset('styles/critical-landing.css');
        $critical_CSS = 'styles/critical-landing.css';
    }

    // if (file_exists($critical_CSS)) {
    if (file_exists(locate_asset($critical_CSS))) {
        echo '<style id="critical-css">' . get_file_contents($critical_CSS) . '</style>';
    }
}, 1);

I’m using a couple of helper functions locate_asset() and get_file_contents() to get a local file path and get the contents of file in a WordPress friendly way using WP_Filesystem. Shoutout to codepuncher and alwaysblank for sharing them on discourse.

First in in /sage/config/assets.php add the following to the array:

'path' => get_theme_file_path().'/dist',

Put these functions in app/helpers.php:

/**
 * Get the absolute path to an asset.
 *
 * @param string $asset
 *
 * @return string
 */
function locate_asset($asset): string
{
    return trailingslashit(config('assets.path')) . sage('assets')->get($asset);
}

/**
 * Get the contents of a file.
 *
 * @param string $asset
 *
 * @return string
 */
function get_file_contents($asset): string
{
    /** @var \WP_Filesystem_Base */
    global $wp_filesystem;

    if (empty($wp_filesystem)) {
        require_once ABSPATH . '/wp-admin/includes/file.php';
    }

    \WP_Filesystem();

    $asset_path = locate_asset($asset);

    if ($wp_filesystem->is_readable($asset_path)) {
        return $wp_filesystem->get_contents($asset_path);
    }

    return '';
}

These helper functions can come in handy for inlining SVGs as well.

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 7,000 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?