Eliminating Unused CSS

RelativeCI - In-depth bundle stats analysis and monitoring - Interview with Viorel Cojocaru

Frameworks like Bootstrap or Tailwind tend to come with a lot of CSS. Often you use only a small part of it and if you aren't careful, you will bundle the unused CSS.

PurgeCSS is a tool that can achieve this by analyzing files. It walks through your code and figures out which CSS classes are being used as often there is enough information for it to strip unused CSS from your project. It also works with single page applications to an extent.

Given PurgeCSS works well with webpack, we'll demonstrate it in this chapter.

Setting up Tailwind#

To make the demo more realistic, let's install Tailwind to the project.

npm add tailwindcss postcss-loader -D

Generate a starter configuration using npx tailwindcss init. After this you'll end up with a tailwind.config.js file at the project root.

To make sure the tooling can find files containing Tailwind classes, adjust it as follows:

tailwind.config.js

module.exports = {
  content: ["./src/**/*.js"],
  theme: {
    extend: {},
  },
  plugins: [],
};

To load Tailwind, we'll have to use PostCSS:

webpack.parts.js

exports.tailwind = () => ({
  loader: "postcss-loader",
  options: {
    postcssOptions: { plugins: [require("tailwindcss")()] },
  },
});

The new configuration still needs to be connected:

webpack.config.js

const cssLoaders = [parts.tailwind()];
const commonConfig = merge([ ...
parts.extractCSS(),
parts.extractCSS({ loaders: cssLoaders }),
]);

To make the project aware of Tailwind, import it from CSS:

src/main.css

@tailwind base;
@tailwind components;
/* Write your utility classes here */
@tailwind utilities;

body {
  background: cornsilk;
}

You should also make the demo component use Tailwind classes:

src/component.js

export default (text = "Hello world") => {
  const element = document.createElement("div");
  element.className = "rounded bg-red-100 border max-w-md m-4 p-4";
  element.innerHTML = text;
  return element;
};

If you run the application (npm start), the "Hello world" should look like a button.

Styled hello
Styled hello

Building the application (npm run build) should yield output:

⬡ webpack: Build Finished
⬡ webpack: asset main.css 1.99 MiB [emitted] [big] (name: main)
  asset index.html 237 bytes [compared for emit]
  asset main.js 193 bytes [emitted] [minimized] (name: main)
  Entrypoint main [big] 1.99 MiB = main.css 1.99 MiB main.js 193 bytes
  orphan modules 261 bytes [orphan] 2 modules
  code generated modules 309 bytes (javascript) 1.99 MiB (css/mini-extract) [code generated]
    ./src/index.js + 1 modules 309 bytes [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./src/main.css 1.99 MiB [code generated]

As you can see, the size of the CSS file grew, and this is something to fix with PurgeCSS.

Enabling PurgeCSS#

purgecss-webpack-plugin allows you to eliminate most of the CSS as ideally we would bundle only the CSS classes we are using.

npm add glob purgecss-webpack-plugin -D

You also need to configure as below:

webpack.parts.js

const path = require("path");
const glob = require("glob");
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");

const ALL_FILES = glob.sync(path.join(__dirname, "src/*.js"));

exports.eliminateUnusedCSS = () => ({
  plugins: [
    new PurgeCSSPlugin({
      paths: ALL_FILES, // Consider extracting as a parameter
      extractors: [
        {
          extractor: (content) =>
            content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [],
          extensions: ["html"],
        },
      ],
    }),
  ],
});
For exceptions, starting from 3.0 PurgeCSS includes safelist and blocklist options.

Next, the part has to be connected with the configuration:

webpack.config.js

const productionConfig = merge();
const productionConfig = merge([parts.eliminateUnusedCSS()]);

If you execute npm run build now, you should see something:

⬡ webpack: Build Finished
⬡ webpack: asset main.css 7.68 KiB [emitted] (name: main)
  asset index.html 237 bytes [compared for emit]
  asset main.js 193 bytes [compared for emit] [minimized] (name: main)
  Entrypoint main 7.87 KiB = main.css 7.68 KiB main.js 193 bytes
  orphan modules 261 bytes [orphan] 2 modules
  code generated modules 309 bytes (javascript) 1.99 MiB (css/mini-extract) [code generated]
    ./src/index.js + 1 modules 309 bytes [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./src/main.css 1.99 MiB [code generated]
  webpack 5.5.0 compiled successfully in 2429 ms

The size of the style has decreased noticeably. Instead of 1.99 MiB, we have roughly 7 KiB now.

Tailwind includes PurgeCSS out of the box and it can be preferable to use that. See Tailwind documentation for more information. The example above is enough to illustrate the idea, and it works universally.
uncss is a good alternative to PurgeCSS. It operates through PhantomJS and performs its work differently. You can use uncss itself as a PostCSS plugin.

Critical path rendering#

The idea of critical path rendering takes a look at CSS performance from a different angle. Instead of optimizing for size, it optimizes for render order and emphasizes above-the-fold CSS. The result is achieved by rendering the page and then figuring out which rules are required to obtain the shown result.

critical-path-css-tools by Addy Osmani lists tools related to the approach.

Conclusion#

Using PurgeCSS can lead to a significant decrease in file size. It's mainly valuable for static sites that rely on a massive CSS framework. The more dynamic a site or an application becomes, the harder it becomes to analyze reliably.

To recap:

  • Eliminating unused CSS is possible using PurgeCSS. It performs static analysis against the source.
  • The functionality can be enabled through purgecss-webpack-plugin.
  • At best, PurgeCSS can eliminate most, if not all, unused CSS rules.
  • Critical path rendering is another CSS technique that emphasizes rendering the above-the-fold CSS first. The idea is to render something as fast as possible instead of waiting for all CSS to load.

In the next chapter, you'll learn to autoprefix. Enabling the feature makes it more convenient to develop complicated CSS setups that work with older browsers as well.

Previous chapter
Separating CSS
Next chapter
Autoprefixing

This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.

Need help?