Inlining critical CSS
Published: Fri Jul 24 2020During a recent (highly recommended) talk by Addy Osmani as part of Chrome’s web.dev/live event, Addy explains how Chloé optimised their website for performance and Google’s core web vitals. There are a lot of great takeaways from the talk, some of which could be implemented into your own project relatively easily; however, one technique stuck out slightly for me.
To dynamically generate the critical CSS we developed a script that runs at build-time extracting all the CSS blocks containing the custom
critical: this
property and inlining them in the head of the page. The inlined CSS rules are removed from the original CSS files, which are loaded with low priority using the media=’print’ technique.
The above is taken from the case study from Chloé’s engineering blog.
The goal of inlining critical CSS is to prevent a flash of unstyled content (FOUC). CSS is a render-blocking resource, meaning that it needs to be downloaded and parsed to create the CSSOM. This is then combined with the DOM to create the render tree, which is used to layout the different elements and feed the paint process which ultimately outputs the pixels to the screen. Having a single large CSS file will delay the start render, as all CSS, regardless of whether it will be used or not will have to be downloaded. Inlining the CSS will also avoid a network request to download the CSS file.
So can I inline all my CSS?
Nope. You should try to keep your initial HTML + CSS under 14KB. Why?
Unfortunately, stylesheets do not support the async
attribute, as <script>
do and a variety of different approaches to asynchronously downloading the stylesheets have been implemented. The simplest and widely supported approach is the media="print"
technique quoted earlier. It might seem ugly, but it works well.
Therefore, given the following SCSS file:
style.scss:
/* this bit is critical */
.list {
width: 100%;
padding: 0;
margin: 0;
}
/* this bit is not critical as it only shows after user interaction or is below the fold */
.list-item {
position: relative;
display: block;
padding: 1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:hover .remove {
opacity: 1;
}
}
We would expect it to output critical CSS and a media="print"
link to the remaining stylesheet.
index.html:
<head>
...
<style>
.list {
width: 100%;
padding: 0;
margin: 0;
}
</style>
<link
rel="stylesheet"
href="/style.css"
media="print"
onload="this.media='all'"
/>
</head>
What’s this magic?
The Chloé engineering team developed a script that extracts the CSS blocks marked with critical: this
. I have seen similar scripts which worked by extracting blocks prefixed by // !critical
or extracting CSS which is generated on the server-side as critical (in the case of universal applications). There are also npm modules that generate the critical CSS using puppeteer, such as penthouse.
But in most cases, I prefer something simpler and I can achieve similarly good results through a simple webpack configuration using chunking and HtmlWebpackPlugin. For starters, I separate critical and non-critical CSS into two SCSS files. From experience, a module would either be critical or not (for example a hero banner may be treated as critical) and all its SCSS should belong in one file anyway. Webpack is then configured to extract all SCSS from files named critical.scss
into a separate chunk.
webpack.config.js:
optimization: {
// ...
splitChunks: {
// ...
cacheGroups: {
criticalStyles: {
name: "critical",
test: /critical\.(sa|sc|c)ss$/,
chunks: "initial",
enforce: true,
},
// ...
},
},
// ...
}
The resultant CSS file (using MiniCssExtractPlugin) then needs to be injected into our HTML using HtmlWebpackPlugin. This requires that HtmlWebpackPlugin is configured with the option inject: false
so that we can create our HTML output. We then add a condition to output the contents of the chunk and thus inline our critical CSS.
index.ejs:
<% for (let index in htmlWebpackPlugin.files.css) { %> <% if
(/critical(\..*)?\.css$/.test(htmlWebpackPlugin.files.css[index])) { %>
<style>
<%= compilation.assets[htmlWebpackPlugin.files.css[index].substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</style>
<% } else { %>
<link
rel="stylesheet"
href="<%"
="`${process.env.CDN_URL}${htmlWebpackPlugin.files.css[index]}`"
%
/>
media="print" onload="this.media='all'"> <% } %> <% } %>
With relatively little effort, we can inline our critical CSS. This technique works both with CSS modules and not and can be easily added to an existing project.
Performance-First React Template
While writing this blog, I decided to clean up my Webpack configurations and build scripts to make them more accessible. This work is available in Performance-first React Template and includes the critical CSS configuration described in this blog and much more if you would be interested in taking a look. Feedback is highly welcome, so feel free to reach out to me on Twitter.
Thank you for reading.
Latest Updates
- Contributing to the Web Almanac 2024 Performance chapterMon Nov 25 2024
- Improving Largest Contentful Paint on slower devicesSat Mar 09 2024
- Devfest 2023, MaltaWed Dec 06 2023
- Learn Performance course on web.devWed Nov 01 2023
- Setting up a Private WebPageTest instanceMon Jun 26 2023