DEV Community

loading...

Implementing Critical CSS on your website

gaijinity profile image Andrew Welch Originally published at nystudio107.com on ・16 min read

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

Andrew Welch / nystudio107

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.

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.

So what exact­ly is Crit­i­cal 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.

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:

  • Push crit­i­cal resources for the ini­tial URL route.
  • Ren­der ini­tial route.
  • Pre-cache remain­ing routes (see Ser­vice­Work­ers and Offline Brows­ing).
  • Lazy-load and cre­ate remain­ing routes on demand.

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

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.

Imple­ment­ing Crit­i­cal 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 critical a URL, 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

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.

Tru­ly Dynam­ic 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.

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Discussion (0)

Forem Open with the Forem app