Implementing Critical CSS on your website
Implementing Critical CSS is an essential part of modern website development, this article shows you how to do it
Andrew Welch / nystudio107
When I mention Critical CSS to many frontend developers, I’m often met with a countenance that’s a subtle mixture of confusion and skepticism. “Oh, you’re one of those guys,” they think. Fringe.
But Critical CSS isn’t an esoteric practice reserved only for neckbeards; it’s a essential part of modern web development. There is quite a bit of confusion over what it is, and what it does, so let’s demystify it.
Our goal is simply to get the webpage to render as quickly as we can for our user. Critical <span>CSS</span> is an important tool to help us reach that goal.
Critical CSS is about courtesy and respect for the visitors of our website. They have taken the bold step of visiting our website — something marketing-types spend tons of time and money to make happen — we owe it to them to reassure them that they’ve made a good decision, and to be respectful of their time.
This means we don’t make them wait. People hate to wait. As the A Pretty Website Isn’t Enough article discusses in detail, they may be visiting our website from all manner of devices and circumstances where patience is not an option.
Critical CSS is one facet of building a performant website to make our users happy, and to make our Search Engine Results Page (SERP) happy. I won’t go into the SEO benefits here, but if you’ve interested, you can read more about it in the Modern SEO: Snake Oil vs. Substance article.
That’s our goal. Now let’s see how to do it.
So what exactly is Critical CSS, Anyway?
Critical CSS is simply a method of extracting only the CSS that is needed to render the “above the fold” content on a website, and then inlining that CSS on our webpage. By doing so, we ensure that the browser can render the page immediately for the user visiting it.
The phrase “above the fold” comes from this thing we used to make out of dead trees called a “newspaper.” Anything that was “above the fold” on a newspaper is what is immediately seen by someone glancing at the front page of a folded newspaper.
So we’ve appropriated this phrase for our modern “Internet of things” world to mean the content that is visible to a user on their device without any scrolling. We want it to load quickly, so we use Critical CSS.
There’s no waiting around for a monstrous 600K framework-based CSS to load, there’s no massive render tree that the browser has to construct, there’s no extra http request needed to load the CSS. It’s all right there.
Then while the user is reading our webpage, happily sipping their coffee, we load the full site CSS asynchronously, 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 inlining CSS. This is not an inherently bad thing; indeed, the smart folks over at Google have made it a mandatory part of the Google AMP standard.
But we’re doing it smartly. The Critical CSS is inlined when they first visit our website. First impressions are incredibly important, after all. After that, since we know they’ve downloaded and cached our full site CSS, we just set a cookie so we don’t need to bother inlining CSS anymore.
This makes it fast. Our goal with building a performant website is to eliminate bottlenecks so that the browser can render our page as quickly as possible, and our visitors can smile.
Even if images and other webpage elements are still loading, at the very least, people will be able to read the text on the website. Anything beats staring at a blank white screen.
Besides, we optimized our image loading as per the Creating Optimized Images in Craft CMS right?
On a typical webpage, the non-gzip ’d size of the Critical CSS usually ranges between 10k to 30k, depending on how crazy the designer has gone with CSS selectors. So it’s not bad at all, and positively minuscule compared to the images and other content a typical webpage loads.
You may have run Google PageSpeed Insights on a website, and wondered why it was complaining about Eliminate render-blocking JavaScript and CSS in above-the-fold content. That’s what we use Critical CSS for.
The CSS part, anyway, for the blocking JavaScript you’ll need to use an async JavaScript loader as described in the LoadJS as a Lightweight JavaScript Loader & Using SystemJS as a JavaScript Loader articles.
Lest you think that http2 will solve this problem for you, it will not. While http2 does support the server-push of critical resources, not all web servers support it yet, and not all web browsers support it yet either.
Even when they do, you still have only one pipe you’re attempting to push these resources down, so we still don’t want to shove our full CSS down the pipe before our website can load. We still need a way to extract just the CSS that is needed to render the “above the fold” content, even with http2.
What we’re doing here is essentially the PRPL Pattern, which is a super-important pattern to use when designing modern websites:
- Push critical resources for the initial URL route.
- Render initial route.
- Pre-cache remaining routes (see ServiceWorkers and Offline Browsing).
- Lazy-load and create remaining routes on demand.
If this all sounds somewhat mental, it really isn’t that hard to do. This website you’re reading now uses it, to good effect:
You will need to be using a frontend workflow automation 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 Frontend Dev Best Practices for 2017 article if you aren’t using one yet.
Then it’s just a matter of setting things up.
Implementing Critical CSS with Craft CMS
So enough talk, how do we make this thing happen? The techniques outlined here will show how we implement Critical CSS with Craft CMS, but the vast majority of it will apply to any CMS system or frontend dev workflow.
The overall technique is simple, we generate a chunk of Critical CSS for each user-facing template. For instance, while there are many blogs on this site, and the content can vary from blog to blog, there is only one blog template.
Similarly, we then only need one Critical CSS for all of the blog pages, because while the content is dynamic and different, the styles applied to them remain constant.
First, we’ll need to use the fantastic critical npm package that leverages penthouse and phantomjs to do the heavy lifting. So npm install --save-dev critical or yarn add critical --dev and away we go!
Give critical a URL, and it renders the actual webpage in a headless browser. Then it scrapes the rendered webpage looking for any CSS that is rendered “above the fold” and returns it to you.
Pretty neat, eh? We just need to do a little setup work to send it the right things.
So let’s set up the major templates that we need Critical CSS for; I do this in my package.json file as per the A Better package.json for the Frontend 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 website. We provide the url (really it should be called a URI or a path, but whatever) that we append to our critical url, and we have the template associated with that page.
Again, all of this is shown in all of its glory in the A Better package.json for the Frontend article, so I won’t replicate it here, but we have constants for all of these things in our package.json to keep things DRY.
Next we’ll set up a gulp task to generate our Critical 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. Typically I only run gulp criticalcss as a final step before deployment, because it can be somewhat lengthy, and so we don’t want to rebuild it every time.
The code above may look a little goofy, why are we calling doSynchronousLoop()? The reason is that every gulp task is asynchronous. This means that if you’re generating a lot of Critical CSS, we’re going to spawn a ton of critical tasks all running at the same time. Which will make your computer cry for mercy.
So instead, we run them one at a time via the handy doSynchronousLoop() function. It’s not that important that you understand how it works, just what it does. You can alternately use the gulp-run-sequence plugin to achieve the same thing if you like. This will be a non-issue for Gulp 4.0 anyway, whenever it is released.
Here’s what doSynchronousLoop() looks like; it’s a generic function that you can use any time you need to do something synchronously 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 worry, it took my brain a bit to figure out how it worked, too. It’s clever.
We pass in the data that we want processed (our pkg.globs.critical array), the processData function that processes the data (our processCriticalCSS() function), and the done callback function that gets called when the synchronous loop is done (our anonymous function that calls callback() to tell gulp that this task is done).
Finally we get to generating some Critical CSS! Here’s what the processCriticalCSS() function 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 parameters we’re passing in to critical warrant explanation:
- src — the URL to the webpage that we want to scrape for Critical CSS
- dest — a file system path where we want the extracted Critical CSS saved; I save this right in my craft/templates directory, right alongside the actual Twig template, but with _critical.min.css appended to the file name
- ignore — we can tell critical to ignore certain CSS selectors if we want (useful for “animation done” selectors that we don’t want included)
- css — an array of file system paths to the CSS we want to use as the “dictionary” of CSS rules it should draw from; you can also omit this, and just have critical figure out the CSS you include on the page if you want
- width & height — the size of the browser you want it to use to render to. I set this to a large square, to be generous with the CSS we extract
That’s it! Put all of the pieces together, and you’ll have Critical CSS generated for each major template on your website. It may not seem like it, but this actually scales pretty well, and is used on some very large websites.
If you want to delve deeper into what you can pass into critical, check out the critical documentation.
So now that we have our CSS, how do we get it into our templates? First, in our layout.twig we’ll need something 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 hellaciously complicated or at least unfamiliar, but it’s not so bad. Let’s break it down.
First, we use my Cookies plugin to get the value of the critical-css cookie (though we could just as easily use JavaScript as well). This just stores whether our full site CSS has been downloaded or not, by storing the version of the CSS in the cookie.
For my websites, I use server-side filename-based cache busting so that when I change the CSS or JS, I can increment this number, and the cache will be broken for people visiting my website, and they’ll get the latest.
All you really need to know about this is that staticAssetsVersion is a number that gets appended to a resource, so site.combined.min.css becomes, say, site.combined.min.762.css. On the server side of things, it strips this number off, and it loads just the site.combined.min.css file, but the number did its job and forced the cache to be busted.
You don’t need to use this exact technique to use Critical CSS, I’m just explaining what it’s doing. We compare staticAssetsVersion to the value stored in the critical-css cookie, and if they don’t match (or the cookie doesn’t exist), then we need to inline our Critical CSS! It’s done this way, rather than a simple boolean, so that we can ensure our Critical CSS is loaded if we’ve changed the CSS on the website.
Then we store the staticAssetsVersion in the critical-css cookie, and set it to last for 7 days, so we don’t bother inlining Critical CSS when they’ve already downloaded the full site CSS. Seven days is pretty reasonable time period to assume that the CSS will stay cached on their device.
Then we declare the block {% block _inline_css %}
for our templates that extend our layout.twig only if Critical CSS should be loaded (more on that later), and we use loadCSS to asynchronously load our full site CSS using their recommended <link rel=""> pattern.
N.B.: With loadCSS as of version 2.0, we only actually need the cssrelpreload.js JavaScript to handle the <link rel="preload"> pattern. The loadCSS.js script itself is only needed if we ever want to call loadCSS() directly via JavaScript, so you can safely remove loadCSS.js from your project if you don’t need it.
Remember, all of this happens only if our critical-css cookie tells us we need to load the Critical CSS. Once the person has loaded the page, we know that the full site CSS has been downloaded and cached on their device, so we just do a regular old <link rel="stylesheet"> for our site CSS.
Finally, we need to give each template that extends our layout.twig a chance to feed in the Critical CSS it wants to use (it’s unique on a per-template basis, remember). 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 function to pull in our minimized Critical CSS into the {% block _inline_css %}
. And we’re done.
If you want to get really clever about it, you can even do it generically, like this:
{{ source (_self.getTemplateName() ~ '_critical.min.css', ignore_missing = true) }}
If this seems like a whole lot of work, remember that once you have done it once, you can replicate it 1,000 times without a whole lot of additional sweat.
And it’s worth it. Make the web a better place for everyone.
Truly Dynamic Content
If you are doing truly dynamic content, such as using a content builder as described in the Creating a Content Builder in Craft CMS article, you can still use Critical CSS.
Often times people think they are creating dynamic content, when they really aren’t, because while the data changes, the CSS rules don’t. Remember, I’m using a content builder for this very blog, but the CSS rules for the above the fold content do not change on a per-blog basis.
But if you truly are doing it in such a way that the above the fold content CSS varies from entry to entry, what you can do is build per-matrix block Critical CSS, and then combine that to build your Critical CSS for each page.
It may sound difficult, but it’s not so bad… give it a shot. I may explore it as a future topic if people are interested.
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Top comments (0)