Critical CSS and Webpack: Automatically Minimize Render-Blocking CSS
Anthony Gore | July 24th, 2017 | 6 min read
"Eliminate render-blocking JavaScript and CSS". It's the one Google Page Speed Insights suggestion that I always get stuck with.
When a web page is accessed, Google wants it to only load what's useful for the initial view, and use idle time to load anything else. That way, the user can see the page as early as possible.
There are many things we can do to minimize render-blocking JavaScript e.g. code splitting, tree shaking, caching and so on.
But what about CSS? For this, we can minimize render-blocking by isolating the CSS needed for above-the-fold content (a.k.a. the critical CSS) and loading that first. We can then load the non-critical CSS afterwards.
Isolating critical CSS is something that can be done programmatically, and in this article I'll show you how to delegate it to your Webpack pipeline.
What does render-blocking mean?
If a resource is "render-blocking", it means the browser can't display the page until the resource is downloaded or otherwise dealt with.
Typically, we will load our CSS in a render-blocking way by linking to our stylesheet in the head
of the document, like this:
<head>
<link rel="stylesheet" href="/style.css">
...
</head>
<body>
<p>I can't be seen until style.css has been loaded!</p>
</body>
When this page is loaded by a web browser, it will read it from top to bottom. When the browser gets to the link
tag, it will start downloading the stylesheet straight away, and will not render the page until it's finished.
For a large site, particularly one with a generously-sized framework like Bootstrap, the stylesheet might be several hundred kilobytes, and the user will have to patiently wait until this fully downloads.
So, should we just link to the stylesheet in the body
, where rendering is not blocked? You could, but the thing is render-blocking is not entirely bad, and we actually want to exploit it. If the page rendered without any of our CSS loaded, we'd get the ugly "flash of unstyled content":
The sweet-spot we want is where we render-block the page with the critical CSS that's required to style the main view, but all non-critical CSS is loaded after the initial render.
Critical CSS
Have a look at this simple page that I've built with Bootstrap and Webpack. This is what it looks like after it's first rendering:
The page also has a modal which is opened by the "Sign up today" button. When opened, it looks like this:
For the first rendering of the page we'll need CSS rules for the nav bar, the jumbotron, the button and a few other general rules for layout and fonts. But we won't need the rules for the modal, since it won't be shown immediately. With that in mind, here's how we might isolate the critical CSS from the non-critical CSS:
critical.css
.nav {
...
}
.jumbtron {
...
}
.btn {
...
}
non_critical.css
.modal {
...
}
If you're on board with this concept, there are two questions that you might now find of interest:
- How can we discern our critical and non-critical CSS programmatically?
- How can we get our page to load the critical CSS before the first render and load the non-critical CSS after the first render?
Example project
I'll briefly introduce you to the basic setup of this project, so when we reach the solution it'll be quick to digest.
Firstly, I'm loading Bootstrap SASS into my entry file.
main.js
require("bootstrap-sass/assets/stylesheets/_bootstrap.scss");
I'm using sass-loader to handle this, and I'm using it in conjunction with the Extract Text Plugin so that the compiled CSS goes into its own file.
I'm also using the HTML Webpack Plugin to create an HTML file in the build. It's necessary for the solution, as you'll soon see.
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
},
...
]
},
...
plugins: [
new ExtractTextPlugin({ filename: 'style.css' }),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
})
]
};
After I run a build, here's what the HTML file looks like. Note that CSS is being loaded in the head
and will therefore block rendering.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vuestrap-code-split</title>
<link href="/style.css" rel="stylesheet">
</head>
<body>
<!--App content goes here, omitted for brevity.-->
<script type="text/javascript" src="/build_main.js"></script>
</body>
</html>
Programmatically identifying critical CSS
Manually identifying the critical CSS would be a pain to maintain. To do it programmatically, we can use Addy Osmani's aptly named Critical. This is a Node.js module that will read in an HTML document, and identify the critical CSS. It does a bit more than that as well, as we'll see shortly.
The way that Critical identifies the critical CSS is by loading the page with PhantomJS, with a screen dimension you specify, and by extracting any CSS rules used in the rendered page.
Here's how we can set it up for this project:
const critical = require("critical");
critical.generate({
/* The path of the Webpack bundle */
base: path.join(path.resolve(__dirname), 'dist/'),
src: 'index.html',
dest: 'index.html',
inline: true,
extract: true,
/* iPhone 6 dimensions, use whatever you like*/
width: 375,
height: 565,
/* Ensure that bundled JS file is called */
penthouse: {
blockJSRequests: false,
}
});
When executed, this will update the HTML file in the Webpack bundle output to:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bootstrap Critical</title>
<style type="text/css">
/* Critical CSS is inlined into the document head, abbreviated here. */
body {
font-family: Helvetica Neue,Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857;
color: #333;
background-color: #fff;
}
...
</style>
<link href="/style.96106fab.css" rel="preload" as="style" onload="this.rel='stylesheet'">
<noscript>
<link href="/style.96106fab.css" rel="stylesheet">
</noscript>
<script>
/*A script for loading the non-critical CSS goes here, omitted for brevity.*/
</script>
</head>
<body>
<!--App content goes here, omitted for brevity.-->
<script type="text/javascript" src="/build_main.js"></script>
</body>
</html>
It will also output a new CSS file e.g. style.96106fab.css (a hash is automatically added to the file name). This CSS file is the same as the original stylesheet, only with critical CSS stripped out.
Inlining critical CSS
You'll notice that the critical CSS has been inlined into the head
of the document. This is optimal as the page doesn't have to load it from the server.
Preload non-critical CSS
You'll also notice that the non-critical CSS is loaded with a sophisticated-looking link
. The preload
value tells the browser to start fetching the non-critical CSS for pending use. But crucially, preload
is not render-blocking, so the browser will go ahead and paint the page whether the preload resource is completed or not.
The onload
attribute in the link
allows us to run a script when the non-critical CSS has eventually loaded. The Critical module automatically inlines a script into the document that provides a cross-browser compatible way of loading the non-critical stylesheet into the page.
<link href="/style.96106fab.css" rel="preload" as="style" onload="this.rel='stylesheet'">
Putting Critical into a Webpack pipeline
I've made a Webpack plugin called HTML Critical Webpack Plugin that is merely a wrapper for the Critical module. It will run after your files have been emitted from the HTML Webpack Plugin.
Here's how you can include it in a Webpack project:
const HtmlCriticalPlugin = require("html-critical-webpack-plugin");
module.export = {
...
plugins: [
new HtmlWebpackPlugin({ ... }),
new ExtractTextPlugin({ ... }),
new HtmlCriticalPlugin({
base: path.join(path.resolve(__dirname), 'dist/'),
src: 'index.html',
dest: 'index.html',
inline: true,
minify: true,
extract: true,
width: 375,
height: 565,
penthouse: {
blockJSRequests: false,
}
})
]
};
Note: you should probably only use this in a production build, not development, as it will make your build really slow!
Performance results
Now that I've isolated critical CSS, and I'm loading the non-critical CSS in idle time, what do I get in the way of performance improvements?
I used the Chrome Lighthouse extension to find out. Keep in mind the metric we're trying to optimise is Time To First Meaningful Paint, which basically tells us how long it is until the user can see something.
Before implementing critical CSS:
After implementing critical CSS:
As you can see, my app got a meaningful paint a full second earlier, and is interactive half a second earlier. In practice, you may not get such a dramatic improvement in your app, since my CSS was thoroughly bloated (I included the entire Bootstrap library), and in such a simple app I didn't have many critical CSS rules.
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...