How I Dropped 250KB of Dead CSS Weight with PurgeCSS

Balloon shedding weight

As a web developer, few things are more satisfying than drastically cutting down the size of your production assets. It‘s like putting your code on a diet and watching it come out lean and mean. I recently had the opportunity to put my site‘s CSS through such a weight loss journey and shed a whopping 250KB of unused styles. Here‘s the story of how I did it.

The Cost of Excess CSS

While 250KB may not sound like a lot in the age of multi-megabyte JavaScript bundles, it‘s still a significant amount of dead weight for a stylesheet. To put it in perspective, the median mobile webpage size as of May 2023 is around 2.1MB according to HTTP Archive. So an extra 250KB of unused CSS represents nearly 12% of the typical mobile page weight!

Every extra byte we send down the wire has a real cost in terms of performance, especially for users on slower connections or limited data plans. Based on global average mobile data costs of $0.11 per MB, those 250KB of wasted CSS could be costing your users an extra 2-3 cents per visit. Multiply that across hundreds or thousands of page views and it adds up.

More importantly, excess CSS bloat slows down page rendering. Using the Lighthouse Performance calculator, we can estimate the impact. Assuming a 4G connection, dropping 250KB from the stylesheet would improve Time to Interactive by around 0.95 seconds. For a 3G connection, the savings jump to 4.1 seconds!

When just a 1 second delay can hurt conversion rates by up to 20%, this is the kind of optimization that has a direct impact on user experience and business metrics. Clearly, keeping your CSS lean is well worth the effort.

Death by a Thousand Utilities

So how did I end up with 250KB of unused CSS in the first place? The culprit, as is often the case these days, was utility classes. Specifically, I was using the very popular and very extensive Tailwind CSS framework.

If you haven‘t used Tailwind before, it‘s a utility-first CSS library that provides hundreds of single-purpose classes you can compose to build out custom UIs. Rather than writing semantic class names, you apply utility classes that map to underlying CSS properties. For example, text-lg for font-size: 1.125rem, mb-4 for margin-bottom: 1rem, and so on.

Tailwind‘s comprehensive set of utility classes is incredibly powerful for rapidly building custom designs without writing much custom CSS. However, therein lies the problem. Tailwind‘s default configuration generates over 10,000 unique utility classes for everything from colors and spacing to transitions and transforms!

Here‘s a small sample of the classes generated for width utilities:

.w-0 { width: 0px; }
.w-1 { width: 0.25rem; }
.w-2 { width: 0.5rem; }
/* ... */
.w-64 { width: 16rem; }
.w-auto { width: auto; }
.w-1\/2 { width: 50%; }
.w-1\/3 { width: 33.333333%; }
/* ... */
.w-full { width: 100%; }
.w-screen { width: 100vw; }  
.w-min { width: min-content; }
.w-max { width: max-content; }
.w-fit { width: fit-content; }

That‘s just a small subset of the width scales Tailwind generates by default. You get similar classes for height, padding, margin, font size, colors, and so on. Tailwind is incredibly granular in its utility classes.

What‘s more, many of these utility classes then get multiplied by responsive breakpoints, hover/focus states, and even arbitrary variants. It‘s quite easy to explode your generated CSS without even realizing it.

For example, here‘s part of my Tailwind config where I added custom responsive and hover variants for border width utilities:

// tailwind.config.js
module.exports = {
  // ...
  variants: {
    borderWidth: [‘responsive‘, ‘hover‘]
  }
}  

This simple addition generates an extra 36 classes like sm:hover:border, lg:border-t-4, xl:border-l-8, etc. You can imagine how quickly this multiplies with all the different utilities and variants available.

All these utility permutations provide incredible flexibility…but 90% of them go unused in any given project. That was certainly the case for my site. The default Tailwind build plus my extra variants ballooned my stylesheet to 259KB! Something had to change.

Pruning Unused Styles with PurgeCSS

I wanted to keep the full power of Tailwind during development but automatically remove the unused styles for production. This is exactly what PurgeCSS was designed for.

PurgeCSS analyzes your content and stylesheets to determine which CSS classes are actually being used. It then strips out any unused classes to produce a much smaller CSS file. Basically, it‘s like tree-shaking for CSS.

Here‘s a simple example. Say you have the following HTML and CSS:

<div class="bg-blue-500 text-white p-4">
  <h1 class="text-2xl mb-2">Hello World</h1>
  <p>This is a blue card with white text.</p>
</div>
.bg-blue-500 { background-color: #3b82f6; }
.text-white { color: #fff; }  
.p-4 { padding: 1rem; }
.text-2xl { font-size: 1.5rem; }
.mb-2 { margin-bottom: 0.5rem; } 
.text-lg { font-size: 1.125rem; }
.w-1\/2 { width: 50%; }

PurgeCSS will scan the HTML, note that text-lg and w-1/2 are not being used, and remove them from the final CSS output:

.bg-blue-500 { background-color: #3b82f6; }
.text-white { color: #fff; }
.p-4 { padding: 1rem; }  
.text-2xl { font-size: 1.5rem; }
.mb-2 { margin-bottom: 0.5rem; }

This is a trivial example but you can imagine how powerful this is for a large, utility-class-heavy stylesheet. Let‘s walk through how I set it up for my Tailwind-based site.

Configuring PurgeCSS

PurgeCSS can be used either as a command line tool or integrated into your build process via plugins for Webpack, PostCSS, Gulp, etc. Since I was using Create React App which hides the Webpack config, I opted to use the CLI version.

First, I installed PurgeCSS:

npm install --save-dev @fullhuman/purgecss  

Then I created a purgecss.config.js file in the project root to configure it:

module.exports = {
  content: [‘./src/**/*.html‘, ‘./src/**/*.js‘],
  css: [‘./src/tailwind.css‘],
  defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
  safelist: {
    standard: [/^ais-/, /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/, /^router-link(|-exact)-active$/],
    deep: [/data-v-.*/]
  }
}

Let‘s break this down:

  • content: An array of glob patterns telling PurgeCSS which files to scan for used classes. This should include your HTML, JavaScript components, etc.

  • css: The path to your generated Tailwind stylesheet.

  • defaultExtractor: A custom extractor function to handle Tailwind‘s specific class name format. The default extractor expects conventional BEM-like classes.

  • safelist: Specific classes and regex patterns to always preserve. I had to safelist some dynamically generated class prefixes like ais-* for Algolia Instant Search as well as some state classes for Vue transitions.

With that in place, I added an npm script to run PurgeCSS and overwrite my Tailwind CSS file with the purged version:

{
  "scripts": {
    "build:css": "tailwind build src/style.css -o src/tailwind.css --purge"
  }
}  

To incorporate this into my production build, I modified my existing script:

{
  "scripts": {
    "build": "npm run build:css && react-scripts build"  
  }
}

Now whenever I run a production build, it will first use Tailwind CLI to generate a full stylesheet, then run PurgeCSS against it to remove unused classes before proceeding with the normal build process. No more manual CSS pruning needed!

Whitelist, Safelist, Classes Preserved

One gotcha with PurgeCSS is that it won‘t detect classes that are added to the DOM dynamically by JavaScript. In my case, I was using a few libraries that injected their own classes at runtime:

To preserve these, I added them to the safelist option in my PurgeCSS config:

safelist: {
  standard: [/^ais-/, /-(leave|enter|appear)(|-(to|from|active))$/]
}  

The standard safelist takes an array of regular expressions. Any classes matching these regexes will be preserved in the final CSS.

As of PurgeCSS 3.0, the whitelist option has been renamed to safelist to be more inclusive, but it works the same way. If you‘re using an older version you‘ll need to use whitelist instead.

Putting PurgeCSS to the Test

With everything setup, it was time to see how much cruft PurgeCSS could clear out. I ran a production build and held my breath as I checked the generated CSS file size.

The results were impressive. My stylesheet went from a bulky 259KB to a lean 9KB after PurgeCSS worked its magic. That‘s a 96.5% reduction in raw size!

PurgeCSS Results

Of course, most of that 259KB would have compressed down nicely with gzip. But even comparing gzipped sizes, PurgeCSS still took my stylesheet from 26KB down to 2KB. Nothing to sneeze at, especially for users on slow connections.

I dug a little deeper into the purged classes to see where the bloat was coming from. Using the excellent CSS Stats tool, I analyzed my original and purged stylesheets.

In the original 259KB version, there were:

  • 9,761 total selectors
  • 5,591 unique class names
  • 832 unique colors
  • 173 media queries
  • 138 unique font sizes

After PurgeCSS, those numbers dropped dramatically:

  • 131 total selectors (98.6% reduction)
  • 102 unique class names (98.1% reduction)
  • 38 unique colors (95.4% reduction)
  • 0 media queries (100% reduction)
  • 11 unique font sizes (92% reduction)

It was eye-opening to see just how many unique utilities were generated that I never ended up using. A whopping 98% of the class names were dead code!

The data also showed some interesting patterns in which types of utilities were most prone to bloat. Tailwind tends to produce a ton of color, spacing and responsive variations by default, very few of which get used. But the core typographic and layout utilities like flex, text-center, font-bold, etc. were much more likely to survive the purge.

Key Takeaways

Going through this process reinforced a few key lessons about managing the size and complexity of stylesheets:

  1. Utility classes are a double-edged sword. While incredibly powerful for rapidly building custom UIs, they can also add a ton of dead code if you‘re not careful. Be judicious about which utilities you enable and how many variants you generate.

  2. Automate the purging process. Trying to manually keep track of which classes are used across a large site is a recipe for bloat. Let tools like PurgeCSS automatically strip unused styles as part of your production build.

  3. Safelist dynamic classes. If you have components or libraries that programmatically add classes, make sure to safelist them with PurgeCSS. Otherwise you might inadvertently remove necessary styles.

  4. Audit your CSS. It‘s eye-opening to dig into CSS stats and see the scale of unused styles. Regularly auditing your stylesheets can reveal opportunities to optimize and prune.

  5. Size matters. While we often obsess over JavaScript bundle sizes, CSS bloat can also have a significant impact on performance. Every KB counts, especially for users on slow connections and limited data plans.

That said, it‘s worth noting that PurgeCSS isn‘t a silver bullet. It doesn‘t eliminate the need for good CSS architecture and hygiene. You still need a maintainable, well-structured styling system. PurgeCSS just helps clean up some of the cruft.

It‘s also not a one-time set it and forget it optimization. As your site changes over time, you may find the PurgeCSS configuration needs updating to safelist new dynamic classes or purge obsolete ones. It‘s an ongoing process, but the automation makes it much more manageable.

All in all though, the savings from incorporating PurgeCSS into my build process were well worth it. Shedding 250KB of dead weight noticeably improved performance and freed up bandwidth for my users. That‘s a win in my book.

So if your own stylesheets are feeling a bit bloated, I highly recommend putting them on a PurgeCSS diet and seeing how much excess you can trim. Your users (and your page speed scores) will thank you!

Similar Posts