Test driving @tailwindcss/jit. SPOILER: It's great

Apr 3, 2021

Recently, there was a lot of noise on Twitter about @tailwindcss/jit.

What’s tailwindcss?

I wanted to see if it lives up to its hype and give it a test drive in my side project. It’s a Phoenix Framework based app in Elixir, with pretty much standard and simple webpack config for front-end assets.

Regarding @tailwindcss/jit, I was wondering about two things:

  • Will the CSS output size stay the same?
  • How will it affect webpack compile times?

Switching from tailwindcss was surprisingly super easy. I was already using purgecss and adding custom CSS only when necessary. I had also inspected CSS output in the past, and I was OK with how it looked liked, pretty minimal, nothing unexpected. So my hope regarding the file size and CSS output was: “stay the same”.

I was more interested in compile times which were pretty large in comparison to everything else. It took WAY too much time for the amount of code there was. I also wasn’t even running purgecss on development, only when building a release for production. In the past this had resulted in few broken deployments until I’ve added more aggressive defaultExtractor matcher to purgecss config.

Below you’ll find a diff of changes I had to make to be able to run “@tailwindcss/jit”, and a comparison in my project. I don’t care about over all benchmark, I care how it works in my case.

I had already been using the newest webpack and purgecss versions so it made upgrade extremely easy, I’m also not using any code splitting because entrypoints are already small.

Used versions

  • @tailwindcss/jit - 0.1.18
  • tailwindcss - 2.0.4
  • purgecss - 3.1.3
  • webpack - 5.28.0

Switch to @tailwindcss/jit diff

diff --git a/.gitignore b/.gitignore
index ea709fe..af62200 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
+.tailwindcss

diff --git a/assets/package.json b/assets/package.json
index 7c1bb49..69e555c 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -2,17 +2,18 @@
   "scripts": {
-    "dev": "yarn run webpack --env development --mode development --no-color",
-    "dev:watch": "yarn run dev --watch"
+    "dev": "TAILWIND_MODE=${TAILWIND_MODE:-build} yarn run webpack --env development --mode development --no-color",
+    "dev:watch": "TAILWIND_MODE=watch yarn run dev --watch"
   },
   "dependencies": {
-    "@fullhuman/postcss-purgecss": "^3.0.0",
+    "@tailwindcss/jit": "^0.1.18",
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
index e4d6a87..267a3b3 100644
--- a/assets/tailwind.config.js
+++ b/assets/tailwind.config.js
@@ -1,6 +1,6 @@
 module.exports = {
-  purge: false,
+  purge: [
+    "../lib/reviewsaurus_web/templates/**/*",
+    "../lib/reviewsaurus_web/views/**/*",
+    "../lib/reviewsaurus_web/live/**/*",
+  ]
diff --git a/assets/webpack.config.js b/assets/webpack.config.js
index 5e613b8..ad2e74c 100644
--- a/assets/webpack.config.js
+++ b/assets/webpack.config.js
@@ -89,20 +89,9 @@ const getConfig = isProduction => ({
       postcssOptions: {
         plugins: [
           "postcss-import",
-          "postcss-preset-env",
           isProduction && require("cssnano"),
-          require("tailwindcss"),
-          isProduction &&
-            purgecss({
-              content: [
-                "../lib/reviewsaurus_web/templates/**/*",
-                "../lib/reviewsaurus_web/views/**/*",
-                "../lib/reviewsaurus_web/live/**/*",
-              ],
-              defaultExtractor: content => content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [],
-            }),
+          require("@tailwindcss/jit")("./tailwind.config.js")
         ].filter(Boolean),
       },
     },

Before @tailwindcss/jit

This is collected on 2nd-3rd run, with cache initialized. I’ve removed most of redundant info from webpack output.

Development (with webpack caching)

default:assets$ yarn run dev
assets by chunk 2.83 MiB (name: main)
  asset ./assets/app.css 2.7 MiB [emitted] (name: main) 1 related asset
  asset assets/app.js 135 KiB [emitted] (name: main) 1 related asset
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 2.83 MiB (863 KiB) = ./assets/app.css 2.7 MiB assets/app.js 135 KiB 2 auxiliary assets
cached modules 127 KiB (javascript) 2.7 MiB (css/mini-extract) 937 bytes (runtime) [cached] 15 modules
webpack 5.28.0 compiled successfully in 403 ms
Done in 1.79s.

CSS highlight, without purgecss:

./assets/app.css 2.7 MiB

That would always nicely freeze browser devtools whenever opened…

On change in CSS file

$ yarn run dev
assets by chunk 2.83 MiB (name: main)
  asset ./assets/app.css 2.7 MiB [emitted] (name: main) 1 related asset
  asset assets/app.js 135 KiB [emitted] (name: main) 1 related asset
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 2.83 MiB (863 KiB) = ./assets/app.css 2.7 MiB assets/app.js 135 KiB 2 auxiliary assets
cached modules 127 KiB (javascript) 937 bytes (runtime) [cached] 13 modules
modules by layer (in null) 50 bytes (javascript) 2.7 MiB (css/mini-extract)
  ./src/css/app.css 50 bytes [built]
  css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./src/css/app.css 2.7 MiB [code generated]
webpack 5.28.0 compiled successfully in 6889 ms
Done in 8.62s.

Looks like most of the cache wasn’t used and everything got rebuilt, or least this whopping 2.7 MiB file.

Production (webpack cache disabled)

$ yarn run deploy
assets by chunk 131 KiB (name: main)
  asset assets/app.js 111 KiB [emitted] [minimized] (name: main) 2 related assets
  asset ./assets/app.css 20.7 KiB [emitted] (name: main)
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 131 KiB (271 KiB) = ./assets/app.css 20.7 KiB assets/app.js 111 KiB 1 auxiliary asset
orphan modules 1.83 KiB [orphan] 4 modules
runtime modules 663 bytes 3 modules
code generated modules 127 KiB (javascript) 32 KiB (css/mini-extract) [code generated]

webpack 5.28.0 compiled with 2 warnings in 9590 ms
Done in 10.76s.

CSS highlight, with purgecss:

./assets/app.css 20.7 KiB

but the build is also slower.

With @tailwindcss/jit

This is collected on 2nd-3rd run, with cache initialized. I’ve removed redundant info from webpack output.

Development (with webpack caching)

default:assets$ yarn run dev
assets by chunk 169 KiB (name: main)
  asset assets/app.js 135 KiB [emitted] (name: main) 1 related asset
  asset ./assets/app.css 34.8 KiB [emitted] (name: main) 1 related asset
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 169 KiB (169 KiB) = ./assets/app.css 34.8 KiB assets/app.js 135 KiB 2 auxiliary assets
cached modules 126 KiB (javascript) 937 bytes (runtime) [cached] 13 modules
modules by layer (in null) 50 bytes (javascript) 34.7 KiB (css/mini-extract)
  ./src/css/app.css 50 bytes [built]
  css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./src/css/app.css 34.7 KiB [code generated]
webpack 5.28.0 compiled successfully in 830 ms
Done in 2.39s.

CSS highlight:

./assets/app.css 34.8 KiB

This will be a bit larger than production build because on production build, we’re also using cssnano to optimize the final output.

On change in CSS file

default:assets$ yarn run dev
assets by chunk 169 KiB (name: main)
  asset assets/app.js 135 KiB [emitted] (name: main) 1 related asset
  asset ./assets/app.css 34.8 KiB [emitted] (name: main) 1 related asset
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 169 KiB (169 KiB) = ./assets/app.css 34.8 KiB assets/app.js 135 KiB 2 auxiliary assets
cached modules 126 KiB (javascript) 937 bytes (runtime) [cached] 13 modules
modules by layer (in null) 50 bytes (javascript) 34.8 KiB (css/mini-extract)
  ./src/css/app.css 50 bytes [built]
  css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./src/css/app.css 34.8 KiB [code generated]
webpack 5.28.0 compiled successfully in 854 ms
Done in 2.41s.

I had made few more changes, and it stays within 850ms +/- 50ms. This means that a change in CSS doesn’t invalidate most of my css webpack cache! We’re talking here about an order of magnitude difference.

Production (webpack cache disabled)

default:assets$ yarn run dev
assets by chunk 133 KiB (name: main)
  asset assets/app.js 111 KiB [emitted] [minimized] (name: main) 2 related assets
  asset ./assets/app.css 22.5 KiB [emitted] (name: main)
asset robots.txt 207 bytes [emitted] [from: static/robots.txt] [copied]
Entrypoint main 133 KiB (271 KiB) = ./assets/app.css 22.5 KiB assets/app.js 111 KiB 1 auxiliary asset
orphan modules 1.58 KiB [orphan] 4 modules
runtime modules 663 bytes 3 modules
code generated modules 126 KiB (javascript) 30.7 KiB (css/mini-extract) [code generated]

webpack 5.28.0 compiled with 2 warnings in 4933 ms
Done in 6.21s.

CSS highlight:

./assets/app.css 22.5 KiB

There’s an additional 2KiB, and I’m still trying to see where exactly the difference is, but it’s impossible to just run diff because selectors are ordered differently. Both versions behave the same but when @tailwindcss/jit is used, I can see it’s leaving some CSS selectors that were previously removed.

Example, purgecss:

[type="button"],
[type="submit"],
button {
  -webkit-appearance: button;
}

@tailwindcss/jit:

[type="button"],
[type="reset"],
[type="submit"],
button {
  -webkit-appearance: button;
}

This is not a large difference by any means and I’ll have to dig deeper into what’s exactly happening here. I don’t have any element with type='reset' in my code, so it shouldn’t be there.

Summary

I was always annoyed by webpack compile times, my fans kicking in on laptop when running a build, and inability of tailwindcss to help in special cases where you might need one off styling that wasn’t predefined in the config.

In my project, when I search for custom styling(style="..."), there are 24 occurrences. I’ve looked over them, and I’ll be able to move all of them into css classes, additionally, with better mobile support.

It looks like @tailwindcss/jit was the missing piece, and I’ll gladly use it on my projects.