I'm on the record (many times over, if you ask my coworkers) as saying critical css is terrible, a step backwards, and just another example of Google throwing their weight around to enforce what they deem relevant for website performance. It is more performant, but I don't think the tradeoffs are worth it. We invented external stylesheets, css preprocessors, css post processors, etc. because we recognized that embedding a ton of ad-hoc css in the head of a document is difficult, gross, and error prone.

However . . .

As with all things I touch in development, I obsess about it. I mean, if we're gonna do it, we might as well do it right. And we definitely shouldn't do it by hand. There are existing libraries for generating critical css, even existing grunt and gulp integrations to work it directly into your build flow. But after looking at possible solutions, I wasn't satisfied with any of them for various reasons. So I rolled my own. Specifically, I wanted a solution that:

  1. worked well, which is to say, that the result placated Google PageSpeed
  2. was easy to use and maintain
  3. integrated with our existing tools (bootstrap, less, grunt, hogan)
  4. was still easy to debug

Point 4 bears specific mention. The idea with critical css is that you extract the critical css and load it in the head, and then load the rest at the end of the body, preferably using something like rel=preload. But separating out specific styles from a large bundle, and then loading the rest of that bundle, as like a partial bundle, across multiple pages just isn't practical. At least for us. Maybe your set up is different. But if you load the critical css in the head of the document, and then load all of your regular css bundle separately, you end up with duplicate styles, which doesn't matter to the user or to the display of the page, but if you're debugging something in your styles, having all sorts of duplicate rules makes it that much harder. I wanted to prevent that. CSS is enough of a pain already.

I settled on the penthouse module to do the actual css generation. We use the async module to loop over a series of pages (actually page config objects) like this:

// eachOf is for iterating the key/value pairs in an object.
async.eachOfSeries(pages, function(route, page, nextPage) {  

and then for each page, we call penthouse with some configuration:

penthouse({  
  url: `http://localhost:8000${ route }`,
  css: `${ options.target }/css/app.css`,
  width: 1200,
  height: 2100,
  forceInclude: [/^\.col-(xs|sm|md|lg)(-offset)?-\d{1,2}/, '.modal'],
  timeout: 120000
}, function(err, criticalCss) {

Let's look at these options. url is more or less self-explanatory. It's the page url to generate critical css for. Penthouse uses puppeteer, which is a headless Chrome API, to render this page and extract the critical css.

css is the path to your css bundle. Penthouse uses this to calculate which rules to include in the generated css. You can alternatively pass cssString.

width and height are the viewport dimensions. The height is significant because it determines what content is "above the fold" (which is a newspaper term for the content visible without unfolding the newspaper). Obviously, the larger this number is, the more critical css you'll have. We're erring on the side of caution here by generating probably more than we need for most browsers. Most of these options have fairly intelligent defaults, so you could just trust that penthouse knows what it's doing. The width is a little less important, but it can affect the output because of media queries. But typically, if your media queries are structured right, using a width in your largest breakpoint should be sufficient.

forceInclude is an optional setting for rules to include whether they appear in above-the-fold content or not. We're force including all the bootstrap grid classes because, for some reason, penthouse doesn't handle them correctly. We often use an 8/4 split on content, and the right rail was being rendered below the main section by the critical css, and then jumping up in to its place when the full css bundle loaded. We also recently noticed an issue where modal content was appearing sort of overtop of the main page content. The .modal class includes a display: none declaration. Once we force included that class, the issue went away. This is just to say that critical css is not an exact science. Sometimes, running penthouse for the same page several times will generate slightly different css.

Finally, timeout is how long penthouse will wait before failing. 120 seconds is way too long. I have no idea why I made it that.

Once penthouse is done, it returns the critical css as a string. Everything to this point has been standard critical css generation. Here's where we do some cool things. First we wrap the critical css in a body.critical selector like this:

var wrapped = `body.critical {  
  ${ criticalCss }
}`;

Css itself doesn't handle nested declarations, but less does, so next we process this string as if it were less. Since css is a valid subset of less, this compiles just fine.

less.render(wrapped, function(err, raw) {  

We do this wrapping to satisfy point 4 above. Applying the critical styles only to body.critical means we can remove the critical styles as easily as removing the critical class from body.

Next we need to adjust some font paths because we load fonts through a cdn, and we have a hogan lambda that renders the correct url. So we do this:

var css = replaceFontPath(raw.css);  

where replaceFontPath looks like this:

const replaceFontPath = (css) => css.replace(/\.\.\/fonts\/([^')]+?)([')])/g, '{{#lambdas.staticUrl}}/fonts/$1{{/lambdas.staticUrl}}$2');  

This ends up replacing something like ../fonts/Vegur-Light.eot with {{#lambdas.staticUrl}}/fonts/Vegur-Light.eot{{/lambdas.staticUrl}}, then when hogan renders the page, it fills out the rest of the url with the correct domain and initial path.

We also replace some image paths in a much more straight forward way:

css = css.replace(/\/assets\/img\//g, CDN);  

Next we need to cleanup some styles that don't make sense:

css = css.replace(/body\.critical body/g, 'body.critical').replace(/body\.critical html/g, 'html');  

Because of how we wrap the generated css, styles on body and html will have selectors that look like body.critical body and body.critical html, and obviously that won't work, so we just replace them with the right things.

If we're generating production css (which we do as part of our build in Travis CI), we next minify the critical css using clean-css.

css = new CleanCss({ compress: options.compress }).minify(css).styles;  

We use hogan to render pages, so we wrap our final css in a style tag:

let tag = `<style id="critical-css">${css}</style>`;  

and then write it into our partials directory:

fs.outputFile(root + '/server/views/partials/critical/' + _.kebabCase(page) + '.html', tag, { encoding: 'utf8' }, function(err) {  

where we can then include it like this:

{{#yield-head}}
  {{> critical/page-name }}
{{/yield-head}}

If you use hogan and this syntax looks different than you're use to, we actually use hogan-express, which builds on hogan's functionality by adding custom yields, as well as filters and lambdas.

Our css generator is ready, but there's one final step. We need some javascript to remove the critical class from the body tag after our main css bundle is done loading so that debugging our main css is easy. But it's also important to be able to debug your critical css easily, so I wrote this script:

;(function($) {
  $(function() {
    if (window.location.search.indexOf('dbcritical') === -1) {
      setTimeout(function() {
        $('body').removeClass('critical');
      }, 8000);
    }
  });
})(jQuery);

If the query string includes dbcritical, we just don't remove the critical class, which means all the critical stylings will remain applied. Ideally this will look the same, at least until you scroll down. If dbcritical isn't present, we wait for a while and then remove the critical class. The timeout is long just to assure that everything is rendered and loaded to attempt to prevent FOUC (flashes of unstyled content). Since this is only for debugging and since the page should look the same with and without the critical class, it doesn't really matter how long the timeout is. We could also use the load event for our css bundle link to remove the class. That would look something like this:

;(function($) {
  $(function() {
    if (window.location.search.indexOf('dbcritical') === -1) {
      $('#css-bundle').on('load', function() {
        $('body').removeClass('critical');
      });
    }
  });
})(jQuery);

I've made a gist with the full generator to give it context: https://gist.github.com/tandrewnichols/250f6406cbec83c1af1ae862541c00e0. This is intended to be used as a task with grunt. We also use task-master, which is why this task (and it's configuration) appears in isolation from other tasks and grunt config.

That's it! Now we've appeased the Google gods and can return to our meager existence and just hope that some day sense will return to the land, and we can all return to just serving external css at the foot of the page. Or maybe css-in-js will win, and it will all be for naught.