tailwindlabs / tailwindcss

A utility-first CSS framework for rapid UI development.
https://tailwindcss.com/
MIT License
83.07k stars 4.21k forks source link

Poor performance as part of a webpack PostCSS build system #2544

Closed khalwat closed 4 years ago

khalwat commented 4 years ago

Using the latest Tailwind CSS (1.8.13 as of this writing), I'm seeing slowness very similar to https://github.com/tailwindlabs/tailwindcss/issues/1620 & also https://github.com/tailwindlabs/tailwindcss/issues/443, the performance of using HMR with webpack-dev-server and webpack 4 or 5 is quite slow.

It takes about 10 seconds on my MacBook Pro 2019 just changing a single color in a .pcss file, and it appears externally that it's rebuilding everything each time. The building of Tailwind CSS seems to have gotten slower and slower as the amount of utilities it includes have gone up.

I'm not sure what the caching implemented in 1.7.2 does, but in a long running process (and maybe it's already doing this but) what if all of the Tailwind-specific imports like:

@import "tailwindcss/base";

...were cached in a long running process, so it just returns the pre-generated blob? I'd imagine you're probably already doing this, but have any instrumentation or profiling been hooked up to the build to determine where the bottlenecks are?

My postcss.config.js looks 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
            }
        })
    ]
};

...and the whole setup is essentially what's here: https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development#tailwind-css-post-css-config

It's not doing anything fancy re: the PostCSS part of the build, but its extremely slow compared to the HRM of JavaScript modules, etc.

I tried removing postcss-preset-env to see if it made a difference, but it doesn't seem to.

Related: https://stackoverflow.com/questions/63718438/webpack-dev-server-slow-compile-on-css-change-with-tailwind

adamwathan commented 4 years ago

Can you create a GitHub repo that we can use to test and benchmark? Setting it up myself means I'm just gonna put this off for months and months unfortunately, hah.

khalwat commented 4 years ago

Here ya go, all packaged up in a Docker container:

https://github.com/nystudio107/tailwind-css-performance

khalwat commented 4 years ago

I did a little more research on this tonight, and removing Tailwind CSS entirely from the project resulted in instantaneous HMR'd CSS.

Narrowing it down further, my .pcss has the following lines:

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Removing @import 'tailwindcss/utilities'; results in the compilation being extremely fast; which I suppose makes sense, because it is generating the most CSS.

If we're just doing a raw import of a pre-generated file here, I'm guessing that it's just due to the massive side of the CSS file, and there may not be room for optimizations here.

Going on this, and some other various issues filed here, I refactored it to split off the .pcss into 4 separate files which I import individually in my app.ts:

import '../css/tailwind-before.pcss';
import '../css/app-before.pcss';
import '../css/tailwind-after.pcss';
import '../css/app-after.pcss';

tailwind-before.pcss has ->

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

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

app-before.pcss has ->

/**
 * 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';

tailwind-after.pcss has ->

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

app-after.pcss has ->

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

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

So all we've done here is taken one large .pcss bundle and chopped it up into 4 separate ones that can be individually recompiled, and thus also individually HMR'd.

This results in near instantaneous rebuilds for any of my .pcss that I may be writing, while also allowing me to use Tailwind CSS utilities, etc. that are generated dynamically as a result of config file changes..

All of the CSS is still imported in the right order, and they all also build into one combined CSS file for a production build.

I'll likely write this up for others who may run into it.

khalwat commented 4 years ago

Wrote the article up in:

Speeding Up Tailwind CSS Builds

michtio commented 4 years ago

@adamwathan this could be nice to be added in the documentation

Also props Andrew! This will help tons of people.

adamwathan commented 4 years ago

I am surprised splitting up the files makes a difference — to me that points to the build process not doing what it should because it means each file is being processed independently which can have surprising side effects in your CSS. The CSS should be fully concatenated into a single file before being processed by PostCSS for everything to work properly. Otherwise you can't use @apply across files for example.

If someone can put together a super minimal simple example that demonstrates the issue that would be helpful, the provided project with the Docker container looks super complicated.

Tailwind builds everything from scratch in < 4s for me so I wonder if something in the configuration is causing Tailwind to run multiple times.

image

khalwat commented 4 years ago

@adamwathan did you see the part about the postcss-import plugin? It looks to me like it might end up parsing each statement in the imported CSS files, looking for other @import statements.

I haven't tried taking postcss-import out of the mix to confirm its impact on this (if any) but it certainly could be a factor.

I realize that we can't @apply across multiple files, but since I'm lumping all component CSS together, I assumed it'd be fine (that's where my @applys would likely be coming from). I've noted this in the article. I haven't run into any other side-effects, but I'll definitely note them if I do.

As for the bare minimum repo, I'll have to leave that for someone else, or me when I have more time to devote to this.

adamwathan commented 4 years ago

Ahh you are doing JS imports for each file rather than PostCSS imports, that makes sense that it would speed things up in that way but comes with the consequence I mentioned of your files being processed in isolation from each other.

khalwat commented 4 years ago

mmm yeah I removed the postcss-import plugin entirely from postcss.config.js:

module.exports = {
    plugins: [
        require('tailwindcss')('./tailwind.config.js'),
    ]
};

...and reduced by app.pcss to just:

body {
 background-color: blue;
}

@tailwind base;
@tailwind components;
@tailwind utilities;

...and the rebuild times are still as slow as mentioned previously, so it's either Tailwind-related or build system-related (or more likely a mix of both).

jan-dh commented 4 years ago

Can only confirm that we're having similar issues. Our build itself is pretty quick but HMR with css being imported through js is very slow. So much it sometimes crashes Chrome 😅

khalwat commented 4 years ago

If the floor here is 3.6s on a modern MacBook Pro, raw execution, no tooling, no build system, no VM/Docker, etc. in real-world situations it's only going to get worse.

When using HMR, especially iteratively, this can slow down the developer experience significantly.

I don't know enough about Tailwind's internals, but perhaps something like a static cache that just returns the utilities rather than regenerating them if the config hasn’t changed would be ideal.

Or perhaps other shortcuts/caching layers could be implemented if process.node_env is development, similar to how WDS does in-memory compilation and takes other shortcuts in development.

Other moonshots: rewrite the core in Rust or the like and bundle it up via WASM. Yeah, I know, it'd need PostCSS and a whole bunch of other things to be also similarly ported over... just seeing the gains from esbuild, and figured I'd put it out there.

adamwathan commented 4 years ago

Yep something we can continue to work on when we’re able to prioritize it. In the mean time I’m 100% open to pull requests that improve performance if you have any ideas you’d like to try 👍🏻

khalwat commented 4 years ago

I hear ya @adamwathan -- I think to do this effectively, the person would need to know Tailwind's internals really well, which unfortunately I'm not able to delve into currently. So I'll use my hack for now, until it can get addressed in core. Thx!

adamwathan commented 4 years ago

For what it's worth I definitely see much faster rebuilds during a watch process than on initial build at least. This is from a simple postcss-cli set up where I'm changing my CSS file to trigger the rebuilds:

image

Under 1s which given how infrequently you typically update CSS with Tailwind feels adequate to me. Would be interesting to see some flame charts of the webpack setup where CSS is imported directly into JS to see if the slowness is within Tailwind or perhaps the result of webpack doing something expensive with such a large bundle of CSS. Would be nice to speed it up either way but we will have less power to do so if it's just that "importing a big CSS file via webpack is slow".

khalwat commented 4 years ago

Man, I wish I was seeing build times anything close to that. Good idea on swapping in a generic import, I'm going to try that now. Will report back.

jan-dh commented 4 years ago

I ended up using a somewhat middleground solution. Using @khalwat 's seperation into seperate tailwindcss files that I import in my javascript entry point, but stil using the @import 'tailwindcss/base'; syntax in those files. Gives me the adventage of still having all my @apply rules available and the only time the hot reload is slow, is when I change my actual tailwind file.

Got my HMR time to this with the setup:

Updating a property in the tailwind config file gives me this:

Version: webpack 4.39.3
Time: 7838ms
Built at: 10/14/2020 3:01:46 PM

Since we're not updating the tailwind config that much, this seems like a workable solution

khalwat commented 4 years ago

So @adamwathan it turns out you're 100% right on this, I did some timings, and there was effectively no difference between:

  1. The default recommended setup using @import 'tailwindcss/utilities'
  2. Importing just a massive CSS file, in this case the bundled, built utilities.css via @import 'tailwindcss/dist/utilities.css
  3. Directly inlining utilities.css into the app.pcss file (just to see if the postcss-import plugin added overhead

TL;DR: we can cut our build process time in half by not generating CSS sourcemaps, and by setting devtool: 'eval-cheap-module-source-map' in the webpack.dev.js config. Tailwind CSS's build times still could be reduced, but the tooling around the massively generated CSS files is what's causing a good bit of the slowdown.

The separated builds described in the article I still think are useful, because it get us down to < 0.5s for the HMR reload times, but the 3.2s HMR times I'm seeing now are at least usable. I need to update the article with these build settings too.

The details:

1 @import'd tailwindcss/utilities:

webpack_1  | Version: webpack 4.44.2
webpack_1  | Time: 7260ms
webpack_1  | Built at: 10/14/2020 1:13:05 PM
webpack_1  |                                  Asset       Size  Chunks                               Chunk Names
webpack_1  | app.eaa21190e6318cfca3af.hot-update.js   8.77 MiB     app  [emitted] [immutable] [hmr]  app
webpack_1  |   eaa21190e6318cfca3af.hot-update.json   45 bytes          [emitted] [immutable] [hmr]
webpack_1  |                              js/app.js   10.8 MiB     app  [emitted]                    app
webpack_1  |                          manifest.json  179 bytes          [emitted]
webpack_1  |  + 2 hidden assets

.....

2 @import'd tailwindcss/dist/utilities.css:

webpack_1  | Version: webpack 4.44.2
webpack_1  | Time: 7524ms
webpack_1  | Built at: 10/14/2020 1:11:11 PM
webpack_1  |                                  Asset       Size  Chunks                               Chunk Names
webpack_1  |   56687676ce73de1152f4.hot-update.json   45 bytes          [emitted] [immutable] [hmr]
webpack_1  | app.56687676ce73de1152f4.hot-update.js   14.5 MiB     app  [emitted] [immutable] [hmr]  app
webpack_1  |                              js/app.js   16.5 MiB     app  [emitted]                    app
webpack_1  |                          manifest.json  179 bytes          [emitted]
webpack_1  |  + 2 hidden assets

.....

3 Inlined utilities.css

webpack_1  | Version: webpack 4.44.2
webpack_1  | Time: 7102ms
webpack_1  | Built at: 10/14/2020 1:09:51 PM
webpack_1  |                                  Asset       Size  Chunks                               Chunk Names
webpack_1  | app.fa2c53f7c35b6db7db11.hot-update.js   14.2 MiB     app  [emitted] [immutable] [hmr]  app
webpack_1  |   fa2c53f7c35b6db7db11.hot-update.json   45 bytes          [emitted] [immutable] [hmr]
webpack_1  |                              js/app.js   16.2 MiB     app  [emitted]                    app
webpack_1  |                          manifest.json  179 bytes          [emitted]
webpack_1  |  + 2 hidden assets

.....

So then I looked and noticed how positively massive the generated hot updates were... 8.77M for #1 @import 'tailwindcss/utilities' and a whopping 14.5M for #2 @import 'tailwindcss/dist/utilities.css

Turns out almost all of this is the sourcemaps, which I had set to inline-source-map for devtool in my webpack config but also, in my configurePostCssLoader I had sourceMap: true for both the postcss-loader and the css-loader.

This is why the #2 @import 'tailwindcss/dist/utilities.css hot update was so huge, it looks like it was generated sourcemaps for both loaders.

Here are some timing tests:

sourceMap: false for css-loader & postcss-loader and devtool: 'inline-source-map'

webpack_1  | Version: webpack 4.44.2
webpack_1  | Time: 4103ms
webpack_1  | Built at: 10/14/2020 1:30:14 PM
webpack_1  |                                  Asset       Size  Chunks                               Chunk Names
webpack_1  |   420f72e6cc40facb1510.hot-update.json   45 bytes          [emitted] [immutable] [hmr]
webpack_1  | app.420f72e6cc40facb1510.hot-update.js   6.53 MiB     app  [emitted] [immutable] [hmr]  app
webpack_1  |                              js/app.js   8.53 MiB     app  [emitted]                    app
webpack_1  |                          manifest.json  179 bytes          [emitted]
webpack_1  |  + 2 hidden assets

....

sourceMap: false for css-loader & postcss-loader and devtool: 'eval-cheap-module-source-map'

webpack_1  | Version: webpack 4.44.2
webpack_1  | Time: 3296ms
webpack_1  | Built at: 10/14/2020 1:59:52 PM
webpack_1  |                                  Asset       Size  Chunks                               Chunk Names
webpack_1  |   7059df1a7293e89c81d1.hot-update.json   45 bytes          [emitted] [immutable] [hmr]
webpack_1  | app.7059df1a7293e89c81d1.hot-update.js   5.99 MiB     app  [emitted] [immutable] [hmr]  app
webpack_1  |                              js/app.js   7.98 MiB     app  [emitted]                    app
webpack_1  |                          manifest.json  179 bytes          [emitted]
webpack_1  |  + 2 hidden assets
goldcoders commented 4 years ago

build time is really slow even with hugo , which under the hood uses esbuild as its compiler which is the fastest compiler... I have one project and building that from cold start is 8secs... compared to non tailwind project around 200ms... As much as i Love tailwind,tailwind is So SLOW, and This Literally Increases Build time Specially for Cloud Providers like netlify. Hope tailwind gets bump to re architecture it, avoiding this slow building process... Me i Dont Use @apply at all, Its always much better to write, your own css ... If that is the reason building takes too many round trip to complete maybe you can have an option to drop a way to use that feature... thus improving speed... tailwind already has good utility classes and its stupid to use @apply , if you need custom classes just write it on plain css... or inline it with html

adamwathan commented 4 years ago

Going to close this now because the underlying issue is that webpack performs poorly with large CSS files and there's just nothing we can do about that. Making Tailwind smaller by default is not really an option as it would severely cripple the usefulness of the framework. This would be better as an issue on the webpack repo around how to improve performance around the handling of large CSS files. Not trying to pass the buck but literally nothing we can do about it other than dedicate resources to making improvements directly to webpack which is not out of the question, but still work that would happen in their project and not in ours.

khalwat commented 4 years ago

Yep, agreed re: closing the issue.

The only thing I'd add is that I think it's a common problem, so if generating less CSS is out of the question, perhaps a direction Tailwind could take is some kind of official way to handle the type of "CSS-splitting" mentioned in the article.

I realize Tailwind is intended to work as one glob that's parsed by PostCSS, but perhaps some kind of official documentation on the problems people can run into re: performance, and the ways around it.

khalwat commented 4 years ago

I got the full Tailwind CSS build -- no CSS splitting, so global @apply -- down to 1s or so on my MacBook Pro:

webpack_1  | example-project (webpack 5.1.3) compiled successfully in 1070 ms
khalwat commented 3 years ago

@jan-dh I'm not sure how adding @import 'tailwindcss/base' makes a difference here? The Tailwind base.css just has some simple CSS reset rules and the like.

I've been able to port a number or projects to use the technique described in my article without ill effect, and with @apply seeming to work fine.

I'm sure there's something you miss out on but splitting it this way, but it seems to be working as expected for me.

¯_(ツ)_/¯

khalwat commented 3 years ago

Just an update here for anyone who might be reading through this issue in the future.

  1. In Tailwind 1.x, the technique described in this issue—and the technique describe in the article I wrote—works great with global @apply with no issues.
  2. In Tailwind 2.x, whatever changes were made to how @apply works internally cause it to no longer work as described in this issue and in the article.
  3. Adding @import 'tailwindcss/base'; as @jan-dh mentioned doesn't change the result in either case.

In addition, the generation and HMR of Tailwind CSS has gotten slower in 2.x as well, but that may be just due to the fact that it's generating more CSS now.

So what @adamwathan mentioned about losing global @apply now makes sense to me; you lose it in Tailwind 2.x, which is likely what he has been steeped in for some time. But global @apply works fine in Tailwind 1.x with this technique.

ref: https://github.com/tailwindlabs/tailwindcss/issues/2820

gregtap commented 3 years ago

This thread is a summary of my last 10h. Thank you for your great work @khalwat. This issue kills the developer experience and jumping to Tailwind 2.0 made it worse. The utilities +postcss precss processing = 15s on a relatively small project.

khalwat commented 3 years ago

@gregtap check out the webpack config linked to ITT, you'll find it significantly faster than 15s -- should get you down to 2.5s or so: https://github.com/tailwindlabs/tailwindcss/issues/2820

oliverw commented 3 years ago

No offense but I'm not sure how anyone working on a remotely real-life sized project is able to deal with the compile times. Compilation takes at least 10 seconds on my system (Ryzen 3900XT), spitting out a huge style chunk:

chunk {main} main.js, main.js.map (main) 500 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 150 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {scripts} scripts.js, scripts.js.map (scripts) 10.1 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 31.5 MB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 4.68 MB [initial] [rendered]
Date: 2020-11-23T17:54:53.269Z - Hash: fa74879a0d1b447afc66 - Time: 32215ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully.

I've checked and 27 MB of the 31 MB styles.js chunk appears to be source-maps.

khalwat commented 3 years ago

@oliverw check my comment here: https://github.com/tailwindlabs/tailwindcss/issues/2820#issuecomment-737227525 for how to eliminate the source-maps

deadcoder0904 commented 3 years ago

Kinda agreed...it's too slow as of v2. Have to wait for 10-15 seconds to see the changes.

tleish commented 3 years ago

@gregtap check out the webpack config linked to ITT, you'll find it significantly faster than 15s -- should get you down to 2.5s or so: #2820

@gregtap - which webpack config are you referring to? Is it referenced somewhere in #2820 ?

michtio commented 3 years ago

@rightaway have you split up your TW files? There is a comment above with an article to speeding up your TW builds.

savvyshell commented 3 years ago

Yep, the hot reload page refreshes take their sweet time. Splitting up the CSS files does help though with build time at the very least.

non25 commented 3 years ago

Have you tried following approach:

?

More context: https://github.com/sveltejs/svelte-preprocess/issues/275#issuecomment-765223806 Svelte template which leverages this approach: https://github.com/non25/svelte-tailwind-template

For those who don't want to use @apply you can just make separate postcss config for building tailwind and drop it from your regular build chain.

You can also make a small loader, which will rebuild tailwind.css on config changes, but won't include it in the bundle.

I would much prefer HMR in <1s with the need to reload page on tailwind.config.js changes to the current situation.

@adamwathan what do you think ? Is there any problems that could arise from this approach ?

Hecatron commented 3 years ago

I noticed slow build times when trying out tailwind 2.0 (with all the latest packages installed via yarn v2) build time of around 18 seconds (18484 ms)

I was able to get some speed ups by enabling the file cache under webpack (for webpack 5) to around 1.8 seconds (1834 ms) after the first build

  cache: {
    type: 'filesystem',
  },

Bearing in mind sometimes if the cache gets corrupt you need to delete .yarn.cache\webpack (using yarn v2 pnp) to straighten stuff out, since this is an experimental feature from what I understand

klausXR commented 3 years ago

This is a terrible issue for larger projects. If I enable dark mode, starting the dev server straight up crashes due to being out of memory, its completely unusable.

Hecatron commented 3 years ago

Another solution I put here https://github.com/tailwindlabs/tailwindcss/issues/2820

and that was to avoid using style-loader and try MiniCssExtractPlugin instead

YoungElPaso commented 3 years ago

Splitting the Tailwind utilities etc worked well enough for me - custom CSS in another file entirely. Cut the build time from about 10-15s to about 2, which is fast enough in my books for now.

Works well enough so can use Storybook 6 and not really feel any pain. Great work and great article, @khalwat , many thanks!

nzozor commented 3 years ago

The latest JIT option in version 2.1 solves this issue for me. https://tailwindcss.com/docs/just-in-time-mode

dingman commented 3 years ago

@nzozor I'm using webpack and postcss, how did you get JIT working with webpack? It was breaking more than doing anything good for me.

nzozor commented 3 years ago

@dingman I'm using Angular and ngx-tailwind schematics which hides the webpack and postcss config. So I just updated tailwind.config.js in my case

khalwat commented 3 years ago

@dingman here's a webpack 5 example:

https://github.com/nystudio107/annotated-webpack-config/blob/webpack-5/buildchain/tailwind.config.js#L3

https://github.com/nystudio107/annotated-webpack-config/blob/webpack-5/buildchain/webpack-configs/postcss-loader.config.js

et-hh commented 3 years ago

对于无法升级到最新版本tailwind的可以通过webpack插件解决这个问题: https://juejin.cn/post/6956424977184718856