loading...
Cover image for Inlining Critical CSS

Inlining Critical CSS

imkevdev profile image Kevin Farrugia Originally published at imkev.dev ・4 min read

Cross-posted from: https://imkev.dev/inlining-critical-css

During 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:

/* 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.

<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 extract 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 which generate the critical CSS using puppeteer, such as penthouse.

But in most cases, I prefer something simpler and I am able to achieve the similarly good results through a simple webpackconfiguration 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.

  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 own HTML output. We then add a condition to output the contents of the chunk and thus inline our critical CSS.

<% 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 are able to inline our critical CSS. This technique works both with CSS modules and not and can be easily added to an existent 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 @imkevdev.

Thank you for reading.

Posted on by:

imkevdev profile

Kevin Farrugia

@imkevdev

I’m Kevin Farrugia, an independent consultant on front-end architecture & web performance.

Discussion

pic
Editor guide
 

So can I inline all my CSS?
Nope. You should try to keep your initial HTML + CSS under 14KB. Why?

Its 2020, TailwindCSS exists, so this answer a lot of the times is yes, because production CSS is very small :D

 

Thank you Paweł. Yes there is (and it is a superb framework), so if your production CSS is tiny, go for it, but in many cases I still think non-critical CSS should be lazy loaded... because why not?

 

To avoid additional request and layout shift.

We should avoid layout shift because the critical CSS would have already been fetched. The non-critical CSS will be applied on elements below the fold or out of sight.

In theory - in practice its usually not the case. ;)