DEV Community

loading...

Speeding Up Tailwind CSS Builds

Andrew Welch
Stackless / Full Server developer @ nystudio107 Mak­ing the web bet­ter one site at a time, with a focus on per­for­mance, usabil­i­ty & SEO
Originally published at nystudio107.com on ・9 min read

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

Discussion (0)