Purging CSS in Ember (PostCSS + Purgecss)

22 November 2019

This is the continuation of my first post, Ember + Tailwind CSS + postcss-import, in which I'll describe how I added Purgecss to the mix to remove unused TailwindCSS classes. If you haven't read it, and want a step-by-step guide to set up Tailwind in your Ember project, I recommend starting there.

Also, Chris Masters has a working example of using Purgecss with PostCSS and Ember.js that has a detailed README, so check out that one, too.

Adding Purgecss to the project

Purgecss is a tool to remove unused CSS rules from the css bundle that gets shipped. It pays a high dividend when you use a framework like TailwindCSS since it provides a ton of rules you'll not use in your app.

It integrates nicely with PostCSS via a plugin, so let's start by adding it to our project:

1$ yarn add -D @fullhuman/postcss-purgecss

Postcss needs to know where to look for files that use CSS rules. It will parse those files, come up with a list of rules used in them, and discard the rest.

 1// ember-cli-build.js
 2const { join } = require('path');
 3// (...)
 4module.exports = function(defaults) {
 5  let app = new EmberApp(defaults, {
 6    // (...)
 7    postcssOptions: {
 8      compile: {
 9        plugins: [
10          { module: require('postcss-import') },
11          require('tailwindcss')('./app/styles/tailwind.js'),
12          require('@fullhuman/postcss-purgecss')({
13            content: [
14              join(__dirname, 'app', 'index.html'),
15              join(__dirname, 'app', 'templates', '**', '*.hbs'),
16              join(__dirname, 'app', 'components', '**', '*.hbs'),
17            ],
18            defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
19          })
20        ],
21      }
22    }
23  });
24  // (...)
25  return app.toTree();
26};

The content parameter should be passed a list of globs that specify those files. The first two lines are the usual places to find templates in. The third one, app/components is needed because I use an Ember 3.13 feature, template co-location, so component templates are found under this folder (it took me a while to realize why some, but not all, Tailwind classes had been stripped :) ).

If you use pods, make sure you also define the folder where pods live.

Overriding the "extractor", the function that decides whether an expression is a CSS rule, is needed because Tailwind also includes variants and they use a colon (e.g xs:hidden, hover:text-blue, and so on), so they need to be parsed as valid rules.

When you restart your Ember server, you'll see the css payload size reduced drastically. In my case it went down from 620KB to 20KB (from 63KB to 5KB, when gzipped)! Better yet, your site looks just as good as before.

Using Purgecss conditionally

We could stop here, but there's one more thing we might want to do. We might only want to use Purgecss in production becase of two things. First, it adds build time to each development cycle, and second, it unused styles are stripped we can't add CSS classes to elements in the Developer console, a very useful technique in front-end development.

To address this, I copied the solution from Chris Master – to purge css only in production:

 1// ember-cli-build.js
 2(...)
 3const isProduction = EmberApp.env() === "production";
 4
 5const purgeCSS = require('@fullhuman/postcss-purgecss')({
 6  content: [
 7    join(__dirname, 'app', 'index.html'),
 8    join(__dirname, 'app', 'templates', '**', '*.hbs'),
 9    join(__dirname, 'app', 'components', '**', '*.hbs')
10  ],
11  defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
12});
13
14module.exports = function(defaults) {
15  let app = new EmberApp(defaults, {
16    // (...)
17    postcssOptions: {
18      compile: {
19        plugins: [
20          { module: require('postcss-import') },
21          require('tailwindcss')('./app/styles/tailwind.js'),
22          ...(isProduction ? [purgeCSS] : [])
23        ]
24      }
25    }
26  });
27  // (...)
28}

The main drawback of this solution I can think of is that development has now drifted from production. Bugs related to missing styles (because of an overly aggressive rule pruning) cannot be caught in development while they might visually break the production site.

One way to address that is through visual regression testing, using a service like Percy, and running the build process in a production-like environment (so that Purgecss will be in the pipeline).

Purgecss as a risk factor

An important aspect when developing software that engineers face day-by-day is what trade-offs to make – I'd even argue it's one of the crucial skills. I was again reminded of this by Edward Faulkner on Twitter:

Nice. My only feedback would be that there are some easy-to-make bugs that you would miss in dev if you only purgecss in prod. For example, if you add a helper that emits some class names, their styles will be missing production but not in dev, so you wouldn't notice in time.

— Edward Faulkner (@eaf4) November 25, 2019

I knew it's always risky to "increase the distances" between dev and production but Ed's point goes further: it's impossible to know whether you haven't broken something visually because of all the dynamism (e.g helpers emitting class names, concatanating class names, etc.). Sure, visual regression testing can help (and I'll make sure to add it regardless of Purgecss) but, just as with non-visual tests, you can only catch what you test. You probably don't have 100% visual test coverage either.

A great case-in-point that broke my app visually was that I styled ember-application and ember-testing-container to give base styles to the whole app (text-gray-800 bg-gray-300 leading-tight antialiased). However, both of these classes are added by Ember at runtime so they don't appear in any template or html that Purgecss scanned – so they were purged. I needed to add all of these class names to the whitelist config option of Purgecss to prevent this.

By introducing Purgecss, you introduce a risk factor that had previously been missing. It might be worth it, or it might not. After considering the trade-offs, you have to make the call.

Share on Twitter