I’m a big advocate for utility-first CSS. After trying several methods over the years, it’s what I found to be the best, most maintainable and scalable way of writing CSS to this day.
The whole point of a library is to give you access to a broad set of tools to use at will. The problem is, since you usually use only a subset of it, you end up with a lot of unused CSS rules in your final build.
In my case, not only did I load the entire Tailwind CSS library, but I also added several variants to some modules. That ended up making the final minified CSS file weight 259 KB (before GZip). That’s quite heavy when you consider the website is a simple single-page app with a minimal design.
You don’t want to load each utility by hand when you need it. That would be a long and cumbersome task. A better scenario is to have everything at your disposal during development and automatically remove what you didn’t use during the build step.
PurgeCSS analyzes your content files and your CSS, then matches the selectors together. If it doesn’t find any occurrence of a selector in the content, it removes it from the CSS file. For the most part, this can work out of the box. However, there are some areas in any website that may require some more thinking before letting PurgeCSS do its magic.
Splitting my CSS
The project contains three main CSS files:
- A CSS reset called normalize.css, included in Tailwind CSS.
- Tailwind CSS, the most substantial part of my CSS codebase.
- Some custom CSS, mostly for styling the InstantSearch components to which I couldn’t add classes.
PurgeCSS can’t detect that I need to keep selectors such as
.ais-Highlight, because the components that use it only show up in the DOM at runtime. Same goes with
In the case of classes starting with
.ais-, we can sort them out with whitelisting. But when it comes to reset styles, selectors are a bit trickier to track down. Plus, the size of
normalize.css is pretty insignificant and isn’t bound to change, so in this case, I decided to ignore the file altogether. Consequently, I had to split styles before running PurgeCSS.
My initial CSS configuration looked like this:
tailwind.src.cssfile with three
App.cssfile with my custom styles.
- An npm script in
package.jsonto build Tailwind CSS right before starting or building the project. Every time this script runs, it outputs a
src, which is loaded in the project.
@tailwind preflight directive loads
normalize.css. I didn’t want PurgeCSS to touch it, so I moved it to a separate file.
Then, I changed my existing
tailwind script in
package.json to build
Finally, I loaded
normalize.css in the project.
Now, I can run PurgeCSS on
tailwind.css without fearing it might strip down needed rulesets.
We used Create React App to bootstrap the website, so Webpack came preconfigured and hidden behind react-scripts. This means I couldn’t access Webpack configuration files unless I ran
npm run eject to get them back and manage them directly in the project.
Not having to manage Webpack yourself has many advantages, so ejecting wasn’t an option. Instead, I decided to use a custom configuration file for PurgeCSS, and an npm script.
I first created a
purgecss.config.js at the root of the project:
contentproperty takes an array of files to analyze to match CSS selectors.
cssproperty takes an array of stylesheets to purge.
Then, I edited my npm scripts to run PurgeCSS:
- I added a
purgecssscript that takes my configuration file and outputs the purged stylesheet in
- I made this script run every time we start or build the project.
Special extractor for Tailwind CSS
Tailwind CSS uses special characters, so if you use PurgeCSS out of the box, it may remove necessary selectors. Fortunately, PurgeCSS allows us to use a custom extractor, which is a function that lists out the selectors used in a file. For Tailwind, I needed to create a custom one:
Whitelisting runtime classes
PurgeCSS can’t detect classes that are generated at runtime, but it lets you define a whitelist. The classes you whitelist remain in the final file no matter what.
The project uses React InstantSearch, which generates components with classes that all start with
ais-. Conveniently, PurgeCSS supports patterns in the form of regular expressions.
Now if I forget to remove a class that I no longer use from
App.css, it will be taken out from the final build, but my InstantSearch selectors will remain safe.
New build, lighter CSS
With this new configuration, my final CSS file has gone from 259 KB to… 9 KB! It’s pretty significant in the context of a whole project, especially since many countries still have slow and unstable Internet, and more and more people browse on their phone while on the move.
Accessibility is also about catering for people with low bandwidth connection. It’s not acceptable not to try and help your users with slower Internet, especially if what you’re making them download is dead code.
That’s worth taking a moment to optimize your build. 🙂