DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

5 1

Speeding Up Tailwind CSS Builds

Speeding Up Tailwind CSS Builds

Learn how to opti­mize your Tail­wind CSS PostC­SS build times to make local devel­op­ment with Hot Mod­ule Replace­ment or Live Reload orders of mag­ni­tude faster!

Andrew Welch / nystudio107

Speeding up tailwind css builds2

Tail­wind CSS is a util­i­ty-first CSS frame­work that we’ve been using for sev­er­al years, and it’s been pret­ty fan­tas­tic. We first talked about it on the Tail­wind CSS util­i­ty-first CSS with Adam Wathan episode of dev​Mode​.fm way back in 2017!

We use it in the base of every project we do, as part of a web­pack build process described in the An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment article.

One issue that we’ve run into recent­ly is that build times can be slow in local devel­op­ment, where you real­ly want the speed as you are build­ing out the site CSS.

This arti­cle will lead you through how to opti­mize your Tail­wind CSS build process so that your hot mod­ule replace­ment / live reload will be fast again, and explains what’s going on under the hood.

So let’s dive in!

Fram­ing the Problem

The prob­lem I was hav­ing was that a sim­ple edit of one of my .pcss files would result in a good 10 sec­ond or more delay as the CSS was rebuilt.

Grant­ed, one of the love­ly things about Tail­wind CSS is that you don’t have to write much cus­tom CSS; most­ly you’re putting util­i­ty class­es in your markup.

So if I want­ed to do some­thing like change the back­ground col­or of a par­tic­u­lar CSS class, instead of the instant feed­back I was used to from web­pack hot mod­ule replace­ment, I’d be wait­ing a good while for it to recompile.

I wrote up Tail­wind CSS issue #2544, and also cre­at­ed a min­i­mal GitHub repo nys­tu­dio107/­tail­wind-css-per­for­mance to repro­duce it.

But then start­ed spelunk­ing a bit fur­ther to see if I could find a way to mit­i­gate the situation.

I found a tech­nique that sped up my build times immea­sur­ably, and you can use it too.

The Prob­lem Setup

My orig­i­nal base Tail­wind CSS set­up looked rough­ly like this:


css
├── app.pcss
├── components
│ ├── global.pcss
│ ├── typography.pcss
│ └── webfonts.pcss
├── pages
│ └── homepage.pcss
└── vendor.pcss

Enter fullscreen mode Exit fullscreen mode

The meat of this is the app.css, which looked like this:


/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
 @import "tailwindcss/base";

/**
 * This injects any component classes registered by plugins.
 *
 */
@import 'tailwindcss/components';

/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@import 'tailwindcss/utilities';

/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';

Enter fullscreen mode Exit fullscreen mode

This app.pcss file then gets import­ed into my app.ts via:


// Import our CSS
import '../css/app.pcss';

Enter fullscreen mode Exit fullscreen mode

This caus­es web­pack to be aware of it, so the .pcss gets pulled into the build pipeline, and gets processed.

And then my postcss.config.js file looked like this:


module.exports = {
    plugins: [
        require('postcss-import')({
            plugins: [
            ],
            path: ['./node_modules'],
        }),
        require('tailwindcss')('./tailwind.config.js'),
        require('postcss-preset-env')({
            autoprefixer: { },
            features: {
                'nesting-rules': true
            }
        })
    ]
};

Enter fullscreen mode Exit fullscreen mode

And we have a super-stan­dard tailwind.config.js file:


module.exports = {
  theme: {
    // Extend the default Tailwind config here
    extend: {
    },
    // Replace the default Tailwind config here
  },
  corePlugins: {},
  plugins: [],
};

Enter fullscreen mode Exit fullscreen mode

The Prob­lem

This set­up above is pret­ty much what is laid out Tail­wind CSS docs sec­tion on Using with Pre­proces­sors: PostC­SS as your pre­proces­sor.

Tail­wind CSS is built using PostC­SS, so it makes sense that if you’re using a build­chain that uses web­pack or Lar­avel Mix (which uses web­pack under the hood) or Gulp that you’d lever­age PostC­SS there, too.

The doc­u­men­ta­tion rec­om­mends that if you’re using the postc­ss-import plu­g­in (which we are, and is a very com­mon­ly used PostC­SS plu­g­in), that you change this:


@tailwind base;
@import "./custom-base-styles.css";

@tailwind components;
@import "./custom-components.css";

@tailwind utilities;
@import "./custom-utilities.css";

Enter fullscreen mode Exit fullscreen mode

To this:


@import "tailwindcss/base";
@import "./custom-base-styles.css";

@import "tailwindcss/components";
@import "./custom-components.css";

@import "tailwindcss/utilities";
@import "./custom-utilities.css";

Enter fullscreen mode Exit fullscreen mode

This is because postcss-import strict­ly adheres to the CSS spec and dis­al­lows @import state­ments any­where except at the very top of a file, so we can’t mix them in togeth­er with our oth­er CSS or @tailwind directives.

When we use @import "tailwindcss/utilities"; instead of @tailwind utilities; all that’s real­ly hap­pen­ing is the tailwindcss/utilities.css file is imported:


@tailwind utilities;

Enter fullscreen mode Exit fullscreen mode

So we’re just side-step­ping the @import loca­tion require­ment in postcss-import by adding a lay­er of indirection.

But we can have a look at node_modules/tailwindcss/dist/ to get a rough idea how large this gen­er­at­ed file is going to be:


❯ ls -alh dist
total 43568
drwxr-xr-x 12 andrew staff 384B Sep 19 11:34 .
drwxr-xr-x 19 andrew staff 608B Sep 19 11:35 ..
-rw-r--r-- 1 andrew staff 11K Oct 26 1985 base.css
-rw-r--r-- 1 andrew staff 3.1K Oct 26 1985 base.min.css
-rw-r--r-- 1 andrew staff 1.9K Oct 26 1985 components.css
-rw-r--r-- 1 andrew staff 1.3K Oct 26 1985 components.min.css
-rw-r--r-- 1 andrew staff 5.4M Oct 26 1985 tailwind-experimental.css
-rw-r--r-- 1 andrew staff 4.3M Oct 26 1985 tailwind-experimental.min.css
-rw-r--r-- 1 andrew staff 2.3M Oct 26 1985 tailwind.css
-rw-r--r-- 1 andrew staff 1.9M Oct 26 1985 tailwind.min.css
-rw-r--r-- 1 andrew staff 2.2M Oct 26 1985 utilities.css
-rw-r--r-- 1 andrew staff 1.8M Oct 26 1985 utilities.min.css

Enter fullscreen mode Exit fullscreen mode

(inci­den­tal­ly, if you look close­ly, you’ll also have learned Adam Wathan’s birthday)

We can see that the utilities.css file weighs in at a hefty 2.2M itself; and while this comes out in the wash for pro­duc­tion builds when you’re using PurgeC­SS as rec­om­mend­ed in Tail­wind CSS docs: Con­trol­ling file size, it can be prob­lem­at­ic for local development.

So but why is this a prob­lem? If we’re just @import​’ing the file, why would this be so slow?

The rea­son twofold:

  1. Although Tail­wind CSS has opti­miza­tions in placeto mit­i­gate it, there’s still a ton of CSS gen­er­a­tion that has to hap­pen each time a change is made in any of your .pcss files. We lumped them all in togeth­er, so they all get rebuild together.
  2. The postcss-import plu­g­in actu­al­ly pars­es any files you @import, look­ing for oth­er @import state­ments in that import­ed file, and it does this on the CSS that the @tailwind direc­tive gen­er­ates, too.

And our result­ing utilities.css file has by default over 100,000 lines of CSS gen­er­at­ed & parsed through:


❯ wc -l utilities.css
  102503 utilities.css

Enter fullscreen mode Exit fullscreen mode

So that’s not good.

The Solu­tion

So what can we do? It’s inher­ent to Tail­wind CSS that it’s going to cre­ate a ton of util­i­ty CSS for you, and that gen­er­a­tion can only be opti­mized so much.

I start­ed think­ing of var­i­ous caching mech­a­nism that could be added to Tail­wind CSS, but I real­ized the right solu­tion was to just lever­age the platform.

I remem­bered an old Comp Sci maxim:

We’re already using web­pack, which adroit­ly han­dles tons of imports of var­i­ous kinds through a vari­ety of load­ers… why not just break our .pcss up into chunks, and let web­pack sort it out?

The Solu­tion Setup

So that’s exact­ly what I did in the solu­tion branch of nys­tu­dio107/­tail­wind-css-per­for­mance.

Now our CSS direc­to­ry looks like this:


css
├── app-base.pcss
├── app-components.pcss
├── app-utilities.pcss
├── components
│ ├── global.pcss
│ ├── typography.pcss
│ └── webfonts.pcss
├── pages
│ └── homepage.pcss
├── tailwind-base.pcss
├── tailwind-components.pcss
├── tailwind-utilities.pcss
└── vendor.pcss

Enter fullscreen mode Exit fullscreen mode

Our app.pcss file has been chopped up into 6 sep­a­rate .pcss files that cor­re­spond with Tail­wind’s base, components, and utilities methodology:


/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
@tailwind base;



/**
 * Here we add custom base styles, applied after the tailwind-base
 * classes
 *
 */



/**
 * This injects any component classes registered by plugins.
 *
 */
@tailwind components;



/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';



/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@tailwind utilities;



/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';

Enter fullscreen mode Exit fullscreen mode

And then these .pcss files then get import­ed into our app.ts via:


// Import our CSS
import '../css/tailwind-base.pcss';
import '../css/app-base.pcss';
import '../css/tailwind-components.pcss';
import '../css/app-components.pcss';
import '../css/tailwind-utilities.pcss';
import '../css/app-utilities.pcss';

Enter fullscreen mode Exit fullscreen mode

Noth­ing else was changed in our con­fig or setup.

This allows web­pack to han­dle each chunk of import­ed .pcss sep­a­rate­ly, so the Tail­wind-gen­er­at­ed CSS (and impor­tant­ly the huge utilities.css) only needs to be rebuilt if some­thing that affects it (like the tailwind.config.js) is changed.

Changes to any of the .pcss files that we write are rebuilt sep­a­rate­ly & instantly.

💥

Bench­mark­ing Prob­lem vs. Solution

I did a few infor­mal bench­marks while test­ing all of this out. I used a 2019 Mac­Book Pro with 64gb RAM.

For the test, all I did was change the background-color: yellow; to background-color: blue; in the css/components/global.pcss file.

When we do a rebuild using the ​“Prob­lem Set­up”, the webpack-dev-server [WDS] out­put in the browser’s Devel­op­er JavaScript Con­sole looks like this:


[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR] - ../src/css/app.pcss
[HMR] App is up to date.

Enter fullscreen mode Exit fullscreen mode

…and it took 11.74s to do this recom­pile on my 2019 Mac­Book Pro.

Notice that it rebuild the entire app.pcss here.

When we do a rebuild using the ​“Solu­tion Set­up”, the webpack-dev-server [WDS] out­put in the browser’s Devel­op­er JavaScript Con­sole looks like this:


[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR] - ../src/css/app-components.pcss
[HMR] App is up to date.

Enter fullscreen mode Exit fullscreen mode

…and it only took 0.52s to do this HMR rebuild.

Notice that it rebuilt only the app-components.pcss here, and deliv­ered just the diff of it to the brows­er in a high­ly effi­cient manner.

This is a nice gain, and makes the devel­op­ment expe­ri­ence much more enjoyable!

Hap­py sail­ing! ≈ 🚀

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
fpaghar profile image
Fatemeh Paghar

Developing effective caching strategies to speed up Tailwind CSS builds poses a challenge. Identifying the right balance between caching for efficiency and refreshing when necessary without introducing stale styles requires thoughtful planning to optimize build times.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Instrument, monitor, fix: a hands-on debugging session

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️