Andrew Welch · Insights · #critical-css #frontend #performance

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Implementing Critical CSS on your website

Imple­ment­ing Crit­i­cal CSS is an essen­tial part of mod­ern web­site devel­op­ment, this arti­cle shows you how to do it

Critical Css Stopwatch

When I men­tion Crit­i­cal CSS to many fron­tend devel­op­ers, I’m often met with a coun­te­nance that’s a sub­tle mix­ture of con­fu­sion and skep­ti­cism. Oh, you’re one of those guys,” they think. Fringe.

But Crit­i­cal CSS isn’t an eso­teric prac­tice reserved only for neck­beards; it’s a essen­tial part of mod­ern web devel­op­ment. There is quite a bit of con­fu­sion over what it is, and what it does, so let’s demys­ti­fy it.

Our goal is simply to get the webpage to render as quickly as we can for our user. Critical CSS is an important tool to help us reach that goal.

Crit­i­cal CSS is about cour­tesy and respect for the vis­i­tors of our web­site. They have tak­en the bold step of vis­it­ing our web­site — some­thing mar­ket­ing-types spend tons of time and mon­ey to make hap­pen — we owe it to them to reas­sure them that they’ve made a good deci­sion, and to be respect­ful of their time.

This means we don’t make them wait. Peo­ple hate to wait. As the A Pret­ty Web­site Isn’t Enough arti­cle dis­cuss­es in detail, they may be vis­it­ing our web­site from all man­ner of devices and cir­cum­stances where patience is not an option.

Crit­i­cal CSS is one facet of build­ing a per­for­mant web­site to make our users hap­py, and to make our Search Engine Results Page (SERP) hap­py. I won’t go into the SEO ben­e­fits here, but if you’ve inter­est­ed, you can read more about it in the Mod­ern SEO: Snake Oil vs. Sub­stance article.

Happy People

That’s our goal. Now let’s see how to do it.

Link ​So what exactly is Critical CSS, Anyway?

Crit­i­cal CSS is sim­ply a method of extract­ing only the CSS that is need­ed to ren­der the above the fold” con­tent on a web­site, and then inlin­ing that CSS on our web­page. By doing so, we ensure that the brows­er can ren­der the page imme­di­ate­ly for the user vis­it­ing it.

The phrase above the fold” comes from this thing we used to make out of dead trees called a news­pa­per.” Any­thing that was above the fold” on a news­pa­per is what is imme­di­ate­ly seen by some­one glanc­ing at the front page of a fold­ed newspaper.

Newspaper Above The Fold

So we’ve appro­pri­at­ed this phrase for our mod­ern Inter­net of things” world to mean the con­tent that is vis­i­ble to a user on their device with­out any scrolling. We want it to load quick­ly, so we use Crit­i­cal CSS.

There’s no wait­ing around for a mon­strous 600K frame­work-based CSS to load, there’s no mas­sive ren­der tree that the brows­er has to con­struct, there’s no extra http request need­ed to load the CSS. It’s all right there.

Then while the user is read­ing our web­page, hap­pi­ly sip­ping their cof­fee, we load the full site CSS asyn­chro­nous­ly, which caches it on their device.

This contributes enormously to perceived performance, a key metric of visitor satisfaction

Before you cast stones at the heretic, yes, we are inlin­ing CSS. This is not an inher­ent­ly bad thing; indeed, the smart folks over at Google have made it a manda­to­ry part of the Google AMP stan­dard.

But we’re doing it smart­ly. The Crit­i­cal CSS is inlined when they first vis­it our web­site. First impres­sions are incred­i­bly impor­tant, after all. After that, since we know they’ve down­loaded and cached our full site CSS, we just set a cook­ie so we don’t need to both­er inlin­ing CSS anymore.

This makes it fast. Our goal with build­ing a per­for­mant web­site is to elim­i­nate bot­tle­necks so that the brows­er can ren­der our page as quick­ly as pos­si­ble, and our vis­i­tors can smile.

Even if images and oth­er web­page ele­ments are still load­ing, at the very least, peo­ple will be able to read the text on the web­site. Any­thing beats star­ing at a blank white screen.

Besides, we opti­mized our image load­ing as per the Cre­at­ing Opti­mized Images in Craft CMS right?

On a typ­i­cal web­page, the non-gzip d size of the Crit­i­cal CSS usu­al­ly ranges between 10k to 30k, depend­ing on how crazy the design­er has gone with CSS selec­tors. So it’s not bad at all, and pos­i­tive­ly minus­cule com­pared to the images and oth­er con­tent a typ­i­cal web­page loads.

Bottleneck

You may have run Google Page­Speed Insights on a web­site, and won­dered why it was com­plain­ing about Elim­i­nate ren­der-block­ing JavaScript and CSS in above-the-fold con­tent. That’s what we use Crit­i­cal CSS for.

The CSS part, any­way, for the block­ing JavaScript you’ll need to use an async JavaScript loader as described in the Load­JS as a Light­weight JavaScript Loader & Using Sys­temJS as a JavaScript Loader articles.

Lest you think that http2 will solve this prob­lem for you, it will not. While http2 does sup­port the serv­er-push of crit­i­cal resources, not all web servers sup­port it yet, and not all web browsers sup­port it yet either.

Even when they do, you still have only one pipe you’re attempt­ing to push these resources down, so we still don’t want to shove our full CSS down the pipe before our web­site can load. We still need a way to extract just the CSS that is need­ed to ren­der the above the fold” con­tent, even with http2.

What we’re doing here is essen­tial­ly the PRPL Pat­tern, which is a super-impor­tant pat­tern to use when design­ing mod­ern websites:

If this all sounds some­what men­tal, it real­ly isn’t that hard to do. This web­site you’re read­ing now uses it, to good effect:

Pagespeed Insights

Eat­ing my own dogfood

You will need to be using a fron­tend work­flow automa­tion tool of some sort; whether that means grunt or gulp or npm scripts is up to you. You can read more about this in the Fron­tend Dev Best Prac­tices for 2017 arti­cle if you aren’t using one yet.

Then it’s just a mat­ter of set­ting things up.

Link Implementing Critical CSS with Craft CMS

So enough talk, how do we make this thing hap­pen? The tech­niques out­lined here will show how we imple­ment Crit­i­cal CSS with Craft CMS, but the vast major­i­ty of it will apply to any CMS sys­tem or fron­tend dev workflow.

The over­all tech­nique is sim­ple, we gen­er­ate a chunk of Crit­i­cal CSS for each user-fac­ing tem­plate. For instance, while there are many blogs on this site, and the con­tent can vary from blog to blog, there is only one blog template.

Sim­i­lar­ly, we then only need one Crit­i­cal CSS for all of the blog pages, because while the con­tent is dynam­ic and dif­fer­ent, the styles applied to them remain constant.

First, we’ll need to use the fan­tas­tic crit­i­cal npm pack­age that lever­ages pent­house and phan­tomjs to do the heavy lift­ing. So npm install --save-dev critical or yarn add critical --dev and away we go!

Critical Logo

Give criticalURL, and it ren­ders the actu­al web­page in a head­less brows­er. Then it scrapes the ren­dered web­page look­ing for any CSS that is ren­dered above the fold” and returns it to you.

Pret­ty neat, eh? We just need to do a lit­tle set­up work to send it the right things.

So let’s set up the major tem­plates that we need Crit­i­cal CSS for; I do this in my package.json file as per the A Bet­ter package.json for the Fron­tend article:

"critical": [
        { "url": "", "template": "index" },
        { "url": "blog/stop-using-htaccess-files-no-really", "template": "blog/_entry" },
        { "url": "blog", "template": "blog/index" },
        { "url": "offline", "template": "offline" },
        { "url": "wordpress", "template": "wordpress" },
        { "url": "404", "template": "404" }
    ],

So defined in this JSON array, we have just 6 major pages on the web­site. We pro­vide the url (real­ly it should be called a URI or a path, but what­ev­er) that we append to our critical url, and we have the template asso­ci­at­ed with that page.

Again, all of this is shown in all of its glo­ry in the A Bet­ter package.json for the Fron­tend arti­cle, so I won’t repli­cate it here, but we have con­stants for all of these things in our package.json to keep things DRY.

Next we’ll set up a gulp task to gen­er­ate our Crit­i­cal CSS. Here’s what mine looks like:

//critical css task
gulp.task('criticalcss', ['css'], (callback) => {
    doSynchronousLoop(pkg.globs.critical, processCriticalCSS, () => {
        // all done
        callback();
    });
});

The ['css'] just ensures that we build our site CSS via the gulp css task before we run criticalcss, so that our CSS is always up to date. Typ­i­cal­ly I only run gulp criticalcss as a final step before deploy­ment, because it can be some­what lengthy, and so we don’t want to rebuild it every time.

The code above may look a lit­tle goofy, why are we call­ing doSynchronousLoop()? The rea­son is that every gulp task is asyn­chro­nous. This means that if you’re gen­er­at­ing a lot of Crit­i­cal CSS, we’re going to spawn a ton of crit­i­cal tasks all run­ning at the same time. Which will make your com­put­er cry for mercy.

So instead, we run them one at a time via the handy doSynchronousLoop() func­tion. It’s not that impor­tant that you under­stand how it works, just what it does. You can alter­nate­ly use the gulp-run-sequence plu­g­in to achieve the same thing if you like. This will be a non-issue for Gulp 4.0 any­way, when­ev­er it is released.

Here’s what doSynchronousLoop() looks like; it’s a gener­ic func­tion that you can use any time you need to do some­thing syn­chro­nous­ly in JavaScript:

// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
    if (data.length > 0) {
        const loop = (data, i, processData, done) => {
            processData(data[i], i, () => {
                if (++i < data.length) {
                    loop(data, i, processData, done);
                } else {
                    done();
                }
            });
        };
        loop(data, 0, processData, done);
    } else {
        done();
    }
}

Don’t wor­ry, it took my brain a bit to fig­ure out how it worked, too. It’s clever.

We pass in the data that we want processed (our pkg.globs.critical array), the processData func­tion that process­es the data (our processCriticalCSS() func­tion), and the done call­back func­tion that gets called when the syn­chro­nous loop is done (our anony­mous func­tion that calls callback() to tell gulp that this task is done).

Final­ly we get to gen­er­at­ing some Crit­i­cal CSS! Here’s what the processCriticalCSS() func­tion looks like:

// Process the critical path CSS one at a time
function processCriticalCSS(element, i, callback) {
    const criticalSrc = pkg.urls.critical + element.url;
    const criticalDest = pkg.paths.templates + element.template + '_critical.min.css';

    $.fancyLog("-> Generating critical CSS: " + $.chalk.cyan(criticalSrc) + " -> " + $.chalk.magenta(criticalDest));
    $.critical.generate({
        src: criticalSrc,
        dest: criticalDest,
        inline: false,
        ignore: [],
        css: [
            pkg.paths.dist.css + pkg.vars.siteCssName,
        ],
        minify: true,
        width: 1200,
        height: 1200
    }, (err, output) => {
        if (err) {
            $.fancyLog($.chalk.magenta(err));
        }
        callback();
    });
}

Some of the para­me­ters we’re pass­ing in to crit­i­cal war­rant explanation:

  • src — the URL to the web­page that we want to scrape for Crit­i­cal CSS
  • dest — a file sys­tem path where we want the extract­ed Crit­i­cal CSS saved; I save this right in my craft/templates direc­to­ry, right along­side the actu­al Twig tem­plate, but with _critical.min.css append­ed to the file name
  • ignore — we can tell crit­i­cal to ignore cer­tain CSS selec­tors if we want (use­ful for ani­ma­tion done” selec­tors that we don’t want included)
  • css — an array of file sys­tem paths to the CSS we want to use as the dic­tio­nary” of CSS rules it should draw from; you can also omit this, and just have crit­i­cal fig­ure out the CSS you include on the page if you want
  • width & height — the size of the brows­er you want it to use to ren­der to. I set this to a large square, to be gen­er­ous with the CSS we extract

That’s it! Put all of the pieces togeth­er, and you’ll have Crit­i­cal CSS gen­er­at­ed for each major tem­plate on your web­site. It may not seem like it, but this actu­al­ly scales pret­ty well, and is used on some very large websites.

If you want to delve deep­er into what you can pass into critical, check out the crit­i­cal doc­u­men­ta­tion.

So now that we have our CSS, how do we get it into our tem­plates? First, in our layout.twig we’ll need some­thing like this:

{# -- CRITICAL CSS -- #}
    {% set cacheVal = getCookie('critical-css') %}
    {% if not cacheVal or cacheVal != staticAssetsVersion or craft.config.devMode %}
        {{ setCookie('critical-css', staticAssetsVersion, now | date_modify("+7 days").timestamp ) }}
        {% block _inline_css %}
        {% endblock %}
        <link rel="preload" href="{{ baseUrl }}css/site.combined.min.{{staticAssetsVersion}}.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
        <noscript><link rel="stylesheet" href="{{ baseUrl }}css/site.combined.min.{{staticAssetsVersion}}.css"></noscript>
        <script>
            {{ source('_inlinejs/loadCSS.min.js') }}
            {{ source('_inlinejs/cssrelpreload.min.js') }}
        </script>
    {% else %}
        <link rel="stylesheet" href="{{ baseUrl }}css/site.combined.min.{{staticAssetsVersion}}.css">
    {% endif %}

This may look hel­la­cious­ly com­pli­cat­ed or at least unfa­mil­iar, but it’s not so bad. Let’s break it down.

First, we use my Cook­ies plu­g­in to get the val­ue of the critical-css cook­ie (though we could just as eas­i­ly use JavaScript as well). This just stores whether our full site CSS has been down­loaded or not, by stor­ing the ver­sion of the CSS in the cookie.

For my web­sites, I use serv­er-side file­name-based cache bust­ing so that when I change the CSS or JS, I can incre­ment this num­ber, and the cache will be bro­ken for peo­ple vis­it­ing my web­site, and they’ll get the latest.

All you real­ly need to know about this is that staticAssetsVersion is a num­ber that gets append­ed to a resource, so site.combined.min.css becomes, say, site.combined.min.762.css. On the serv­er side of things, it strips this num­ber off, and it loads just the site.combined.min.css file, but the num­ber did its job and forced the cache to be busted.

You don’t need to use this exact tech­nique to use Crit­i­cal CSS, I’m just explain­ing what it’s doing. We com­pare staticAssetsVersion to the val­ue stored in the critical-css cook­ie, and if they don’t match (or the cook­ie does­n’t exist), then we need to inline our Crit­i­cal CSS! It’s done this way, rather than a sim­ple boolean, so that we can ensure our Crit­i­cal CSS is loaded if we’ve changed the CSS on the website.

Yummy Cookies

Have a cookie

Then we store the staticAssetsVersion in the critical-css cook­ie, and set it to last for 7 days, so we don’t both­er inlin­ing Crit­i­cal CSS when they’ve already down­loaded the full site CSS. Sev­en days is pret­ty rea­son­able time peri­od to assume that the CSS will stay cached on their device.

Then we declare the block {% block _inline_css %} for our tem­plates that extend our layout.twig only if Crit­i­cal CSS should be loaded (more on that lat­er), and we use load­C­SS to asyn­chro­nous­ly load our full site CSS using their rec­om­mend­ed <link rel=""> pattern.

N.B.: With load­C­SS as of ver­sion 2.0, we only actu­al­ly need the cssrelpreload.js JavaScript to han­dle the <link rel="preload"> pat­tern. The loadCSS.js script itself is only need­ed if we ever want to call loadCSS() direct­ly via JavaScript, so you can safe­ly remove loadCSS.js from your project if you don’t need it.

Remem­ber, all of this hap­pens only if our critical-css cook­ie tells us we need to load the Crit­i­cal CSS. Once the per­son has loaded the page, we know that the full site CSS has been down­loaded and cached on their device, so we just do a reg­u­lar old <link rel="stylesheet"> for our site CSS.

Final­ly, we need to give each tem­plate that extends our layout.twig a chance to feed in the Crit­i­cal CSS it wants to use (it’s unique on a per-tem­plate basis, remem­ber). Here’s what it looks like in my blog/_entry.twig template:

{% block _inline_css %}
    <style>
        {{ source ('blog/_entry_critical.min.css', ignore_missing = true) }}
    </style>
{% endblock %}

All this is doing is using the Twig source func­tion to pull in our min­i­mized Crit­i­cal CSS into the {% block _inline_css %}. And we’re done.

If you want to get real­ly clever about it, you can even do it gener­i­cal­ly, like this:

{{ source (_self.getTemplateName() ~ '_critical.min.css', ignore_missing = true) }}

If this seems like a whole lot of work, remem­ber that once you have done it once, you can repli­cate it 1,000 times with­out a whole lot of addi­tion­al sweat.

And it’s worth it. Make the web a bet­ter place for everyone.

Link Truly Dynamic Content

If you are doing tru­ly dynam­ic con­tent, such as using a con­tent builder as described in the Cre­at­ing a Con­tent Builder in Craft CMS arti­cle, you can still use Crit­i­cal CSS.

Often times peo­ple think they are cre­at­ing dynam­ic con­tent, when they real­ly aren’t, because while the data changes, the CSS rules don’t. Remem­ber, I’m using a con­tent builder for this very blog, but the CSS rules for the above the fold con­tent do not change on a per-blog basis.

But if you tru­ly are doing it in such a way that the above the fold con­tent CSS varies from entry to entry, what you can do is build per-matrix block Crit­i­cal CSS, and then com­bine that to build your Crit­i­cal CSS for each page.

It may sound dif­fi­cult, but it’s not so bad… give it a shot. I may explore it as a future top­ic if peo­ple are interested.