How to add Critical CSS to a WordPress site

Like most front-end developers, I want my sites to load faster than fast, ideally before the user has even thought about going to my site… :-)

And like probably most front-end developers, I know about the techniques I can use (CSS in the head, JS at end of the body, sprite my icons, optimize my images, set cache headers for my assets, yadda-yadda-yadda…), and I do incorporate the ones that are “easy”, but some are just… harder. Like putting the critical (“above-the-fold”) CSS in-page. Especially when you have a WordPress site…

Well, I have finally decided to take on the task of adding critical CSS to the Netbiscuits company website, and I thought I would document the process as I go. So, sit back, have a nice leisurely read, and enjoy my pain and frustration… :-)

TL;DR, get to it…

Planning

When you are talking about WordPress, everything works around the tried-and-true, well-known procedure:

  1. The Famous 5-Minute Install
  2. Pick a Theme
  3. Pick a few Plugins, if you want
  4. Manually edit CSS, JS and PHP files, if you need
  5. And FTP them to your web host’s server

Well, in a process like that, the typical methods you see for adding critical CSS to a site don’t easily work. Those methods typically involve a Grunt or Gulp process, which requires a Node.js installation, and you would probably also use a Build Process and a Deployment Process as well. That’s a lot of automation, and none of it fits into those nice, neat little steps listed above…

So, as I see it, these are your options for adding the critical CSS to a WP site:

  • Manual. Hunting and pecking to find what you need. This is going to be laborious, but is also going to give you the most efficient CSS, but is also going to be a nightmare to maintain.
  • Browser extension. There are a few out there, but none that I saw were configurable, and none created CSS that worked well for me.
  • Task runner. If you already have a task runner in place, this is pretty easy. If not, it’s a bit more work, but in the end you have an automated solution that you can easily maintain.

While the idea of manually crafting the critical CSS is attractive because of the lean, “perfect” CSS I could craft, such manual work pains me to think about… Especially maintaining it…

And as I said above, no browser extension reliably created anything I liked.

So using a task runner was my choice.

Research

I first had to decide which critical CSS methods I wanted to consider. I quickly boiled things down to two big names (which in my mind means greater crowdsourcing and better long-term upkeep). I decided to look into:

  1. criticalcss, by The Filament Group, and
  2. critical, by

And since I already use, and want to continue using, Grunt for this project, those two URLs get transmutated into:

  1. grunt-criticalcss, still by The Filament Group, and
  2. grunt-critical, by (ported from Addy’s version)

Decision

There is not much difference between Addy’s and the The Filament Group’s versions, so I was initially stymied. I like how Addy’s version handled multiple source and destination files, while sharing a single options object, like:

critical: {
  dist: {
    options: {
      base: './',
      dimensions: [{
        width: 1300,
        height: 900
       },
       {
        width: 500,
        height: 900
      }]
    },
    files: [
      {src: ['index.html'], dest: 'dist/index.html'},
      {src: ['blog.html'], dest: 'dist/blog.html'}
      {src: ['about.html'], dest: 'dist/about.html'}
      {src: ['contact.html'], dest: 'dist/contact.html'}
    ]
  }
}

Or even using wildcards, like:

critical: {
  dist: {
    options: {
      base: './',
      dimensions: [{
        width: 1300,
        height: 900
      },
      {
        width: 500,
        height: 900
      }],
      src: '*.html',
      dest:  'dist/'
    }
  }
}

Both code samples blatantly copied-and-pasted directly from Understanding Critical CSS

Whereas The Filament Group’s version requires a separate object for each source that should be examined, requiring the options object to be replicated for each source, like:

criticalcss: {
  home: {
    options:  {
      outputfile : 'css/critical/critical-home.css',
      filename : 'all.css',
      url : 'http://fgwebsite.local'
    }
  },
  services: {
    options:  {
      outputfile : 'css/critical/critical-services.css',
      filename : 'all.css',
      url : 'http://fgwebsite.local/services/'
    }
  },
  about: {
  ...

Code sample blatantly copied-and-pasted directly from How we make RWD sites load fast as heck

I suppose this configuration could actually be good, if your options were different for some sources, but it also feels like a DRY violation…

However, since all of the tutorials that I liked use The Filament Group’s version, I decided to go with The Filament Group’s grunt-criticalcss.

Setting-up Grunt

Before we can even start we need to install Grunt and that means we have to install Node.js.

Now we can start customizing things for critical CSS! And as I mentioned, I will be borrowing bits and pieces from several of the Additional Resources I listed above.

Install criticalcss

To start we need to add The Filament Group’s grunt-criticalcss to our package.json file (you will see it hiding amongst the other Grunt plugins I use for this project):

{
  "name": "netbiscuits-theme",
  "version": "0.1.1",
  "private": true,
  "devDependencies": {
    "grunt": "~0.4.2",
    "grunt-bump": "^0.5.0",
    "grunt-contrib-concat": "~0.5.1",
    "grunt-contrib-less": "~1.0.0",
    "grunt-contrib-uglify": "~0.7.0",
    "grunt-contrib-watch": "~0.6.1",
    "grunt-criticalcss": "^0.6.0",
    "grunt-grunticon": "~1.4.0",
    "grunt-svgmin": "~2.0.0"
  }
}

Then we run npm install again to make sure we have everything installed.

Configure criticalcss

Next, I worked my way through Jeremy Keith’s article, but since I have multiple Templates, each require their own critical CSS file. This is where I jumped to Joe Watkin’s article: in my Gruntfile.js, I added a new Task (criticalcss) to the grunt.initConfig, and then added a new set of options for each template ('knowledge-base', 'pricing-plan', etc.):

grunt.initConfig({
  ...
  criticalcss: {
    'knowledge-base' : {
      options:  {
        outputfile : 'dist/wp-content/themes/netbiscuits/css/critical/knowledge-base.css',
        filename : 'dist/wp-content/themes/netbiscuits/css/knowledge-base.min.css',
        url : 'http://netbiscuits.local/knowledge-base/netbiscuits-analytics-marketers-guide/'
      }
    },
    'pricing-plan' : {
      options:  {
        outputfile : 'dist/wp-content/themes/netbiscuits/css/critical/pricing-plan.css',
        filename : 'dist/wp-content/themes/netbiscuits/css/pricing-plan.min.css',
        url : 'http://netbiscuits.local/pricing-plan/'
      }
    },
    'global': {
      options:  {
        outputfile : 'dist/wp-content/themes/netbiscuits/css/critical/global.css',
        filename : 'dist/wp-content/themes/netbiscuits/css/global.min.css',
        url : 'http://netbiscuits.local/'
      }
    },
    ...
  },
  ...
});

In the above, for each Template, we are telling criticalcss to fetch the existing CSS (filename), compare it to a page (url), and save to a new file (outputfile) only the CSS that is visible above the scroll.

Then we load the Task along with all of our other Grunt Tasks:

...
grunt.loadNpmTasks('grunt-criticalcss');
...

And register it, along with all of our other Grunt Tasks:

...
grunt.registerTask('default', [... 'criticalcss', ...]);
...

Run criticalcss

Now, if we run Grunt via a command line (grunt), or just this Task specifically (grunt criticalcss), we should see new files in the dist/wp-content/themes/netbiscuits/css/critical directory.

Adding Critical CSS to the Templates

Now that we have our critical CSS files, it’s time to make use of them and add them to our WP templates! Continuing our dance between Jeremy Keith’s and Joe Watkins’ processes…

From Jeremy’s article, I got the basis for using a browser cookie to determine if the browser has already downloaded the actual CSS file, and thus whether to add the critical CSS to the page or to simply a link to that CSS file. The presence of this cookie, however, could prevent a user’s browser from downloading an updated CSS file, so Jeremy also adds a cache-buster to be able to force the browser to download the updated CSS file.

And from Joe’s article, I got the basis for adding multi-template support.

In my functions.php, I added the following (note that I manually define a cache-buster; ideally this is automated, but for this tutorial I’m hard-coding):

// define cache-buster: YYYYMMDD[MMSS]
define( 'NB_SITE_VERSION', '20160113' );

// create global variable to receive stylesheet references from the templates
$nb_stylesheet_queue = array();

// receives css file name and pushes it into the above array
if ( ! function_exists( 'nb_enqueue_stylesheet' ) ) :
  function nb_enqueue_stylesheet( $style ) {

    // get global array
    global $nb_stylesheet_queue;

    // push $style into array
    $nb_stylesheet_queue[] = $style;

  } // nb_enqueue_stylesheet
endif; // function_exists

// enqueue global CSS
nb_enqueue_stylesheet( 'global' );

// add either critical css or link to css file
if ( ! function_exists( 'nb_add_css_to_page' ) ) :
  function nb_add_css_to_page() {

    // get global array
    global $nb_stylesheet_queue;

    // loop through all enqueued stylesheets
    foreach( $nb_stylesheet_queue as $style) {

      // get the full css file URL
      $fullstyle = THEME_DIRECTORY . 'css/' . $style . '.' . NB_SITE_VERSION . '.min.css';

      // check if the user has the full CSS file
      if ($_COOKIE['nb_csscached'] === NB_SITE_VERSION) {

        // if they have the cookie, then they have the CSS file cached, so simply enqueue it
        wp_enqueue_style( 'nb_css', $fullstyle );

      } else {

        // if not, write the critical CSS into the page
        echo '<style>';
          include( TEMPLATEPATH . 'css/critical/' . $style . '.' . NB_SITE_VERSION . '.min.css' );
        echo '</style>'.PHP_EOL;

        // add a minified-version of loadCSS; note the PHP variables mixed in there
        echo "<script>!function(e,t){'use strict';function s(s){function n(){var t,s;for(s=0;s-1&&(t=!0);t?r.media='all':e.setTimeout(n)}var r=t.createElement('link'),i=t.getElementsByTagName('script')[0],a=t.styleSheets;return r.rel='stylesheet',r.href=s,r.media='only x',i.parentNode.insertBefore(r,i),n(),r}s('".$fullstyle."'),t.cookie='nb_css=".NB_SITE_VERSION.";expires=\"Tue, 19 Jan 2038 03:14:07 GMT\";path=/'}(this,this.document);</script>".PHP_EOL;

        // and add a noscript-wrapped CSS link for non-JS users
        echo '<noscript><link rel="stylesheet" href="'.$fullstyle.'"></noscript>'.PHP_EOL;

      }

    } // foreach

  } // nb_add_css_to_page

endif; // function_exists

In my page template files, I added something like this before calling get_header();:

nb_enqueue_stylesheet( 'knowledge-base' );

In my header.php, inside the head, I added this:

nb_add_css_to_page();

Lastly, in my .htaccess, I added a small rewrite to deal with the cache-buster (this is explained quite well in Jeremy’s article, which is also where I copied this code from; it also conveniently deals with any JS files that use the cache-buster):

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+).(d+).(js|css)$ $1.$3 [L]

In the above code, note that I use THEME_DIRECTORY for the URL of the CSS file (used for $fullstyle), but TEMPLATEPATH for the file path of the CSS file (used for the PHP include).

Additionally, I make use of The Filament Group’s loadCSS to asynchronously append the full CSS file for the browser to cache, and set a cookie that this has been done (which includes two PHP variables).

Lastly, a link to the full CSS file is also included inside of a noscript tag, just in case there is no JS.

How this all Works

The first time a visitor comes to the site…

  1. the PHP checks for the cookie and sees none…
  2. so it includes the critical CSS file contents into the page…
  3. then the loadCSS dynamically appends the full CSS file via an async link
  4. and adds a browser cookie to indicate that the full CSS file should be in browser cached…
  5. then the page continues loading normally…
  6. and the user benefits from having the critical CSS in-page.

And on the next time page load…

  1. the PHP checks for the cookie and finds one…
  2. so it enqueues a link to the full CSS file(s)…
  3. then the page continues loading normally…
  4. and the user benefits from having the full CSS file cached by the browser.

And in any case, if the user does not have JS for whatever reason, the full CSS file is fetched via the noscript block.

Summary

Altering the standard WP set-up is never an easy undertaking. In my attempt to add support for inline critical CSS to a WP site, I made use of The Filament Group’s CriticalCSS Grunt task, and borrowed and altered code from several other developers’ articles. I also made sure users that have visited the site before, and have the site’s full CSS in their browser’s cache, do not get the critical CSS inlined again, and made sure I am able to force users to download new versions if the old version has been updated.

The process wasn’t always easy, and it seldom is, but isn’t that part of the fun?

In the end, however you choose (or need) to get your site’s critical CSS, your users will greatly appreciate any efforts you take to make your site load more quickly. Whether they know it or not! :-)

Happy CSSing,
Atg

4 Responses to How to add Critical CSS to a WordPress site

  1. Josh Holmes says:

    Thanks very much for this article, I’m looking forward to trying it all out! It does look a little daunting but should be fun to have a go!

    Kind Regards

    Josh

    • Thanks, Josh. It was a little bit of work to get up and running, definitely easier if you are already familiar with any of the components, but either way, it is totally worth the effort!

  2. Lee Jameson says:

    Hi Aaron,
    You can easily just get the critical css part for a website with free online services like sitelocity.com

  3. Rob says:

    You are 9 and 7 points away from 100 on Page Speed Insights and the “Eliminate render-blocking JavaScript and CSS in above-the-fold content” message is not given, so I assume you use this for your own WP powered site, and very much so it is working great!

    I find all this tuning above the fold content really interesting.
    Thank you for your post, I will surly go over it again when testing this myself.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.