I’ve been using Tailwind CSS for some of my Rails apps with great delight over the last few months. One downside of utility-first CSS frameworks, however, is the fact that they ship with a myriad of potentially useful classes of which your HTML might use but a few percent.
PurgeCSS (which is available as a PostCSS plugin) to the rescue! To simply quote the docs, it
analyzes your content and your css files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your css, resulting in smaller css files.
Install PurgeCSS
Okay, without further ado, let’s add it to the javascript package of our (webpacker enabled) Rails app:
$ bin/yarn add @fullhuman/postcss-purgecss
Since you probably do not want to enable it in your development environment - it does take some time to go through all your code and strip out the unused selectors every time - you’ll probably configure your postcss.config.js
like this:
if (
process.env.RAILS_ENV === "production"
) {
environment.plugins.push(
require("@fullhuman/postcss-purgecss")({
content: [
"./app/views/**/*.html.slim",
"./app/helpers/**/*.rb"
],
defaultExtractor: content => content.match(/[A-Za-z0-9-_:./]+/g) || [],
whitelist: collectWhitelist(),
whitelistPatterns: [],
whitelistPatternsChildren: [/trix/, /attachment/]
})
);
}
module.exports = environment;
Only, that doesn’t quite lead to the expected results with the Slim templating language. Slim is indentation-based and very terse, you basically write out CSS selectors in a tree:
.flex.justify-end.items-center
.flex-1.text-base
.flex-shrink.text-sm
/ ...
What’s more, Tailwind uses a lot of, let’s say unusual characters inside its CSS classes, such as /
, :
, and even .
. Working with those in Slim is hard enough (and one reason I switched back to ERB for most Tailwind-based projects, but sometimes you just don’t have that choice), but getting the correct extractor
regex pattern is some orders of magnitude harder.
Convert It Back To ERB
So, how can we help that? I decided to go with the naivest approach there possibly is, namely, converting Slim templates back to ERB before the webpacker assets are compiled. I was hesitant to try this until I realised slim offers a dedicated erb_converter
for these purposes. So I wrote a custom script, and placed it in lib/scripts/slim_erb.rb
:
require "slim/erb_converter"
open(Rails.root.join("tmp/compiled.html.erb"), "a+") do |compiled|
Dir.glob(Rails.root.join("app/views/**/*.html.slim")).each do |slim_template|
open slim_template do |f|
slim_code = f.read
erb_code = Slim::ERBConverter.new.call(slim_code)
compiled.puts erb_code
end
end
end
This will concatenate all your Slim files into one large ERB file placed in your app’s tmp
folder - which, since we only care about the selectors used in there - is just good enough. What remains to do, is to swap the content
key in the PurgeCSS config:
"./app/views/**/*.html.slim" => "./tmp/*.html.erb"
In your deployment script, you just have to make sure you invoke this script as a Rails runner script (and optionally delete the temporary file):
$ bundle exec rails r lib/scripts/slim_erb.rb && bundle exec rails webpacker:compile
Fingers crossed 🤞🏻, your purged CSS should now contain all the selectors present in your view templates. Of course, the usual caveats about whitelisting third party libraries (like trix
, for example), still apply.
Post Scriptum
I can foresee some objections as to how Tailwind allows for all types of sophisticated configurations (and might already have PurgeCSS included, depending on your version). I used Tailwind mainly as a stand-in for any kind of utility-first CSS framework. I’ve used this technique successfully with Semantic UI, Bootstrap, and others.