Closed khalwat closed 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.
Here ya go, all packaged up in a Docker container:
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.
Wrote the article up in:
@adamwathan this could be nice to be added in the documentation
Also props Andrew! This will help tons of people.
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.
@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 @apply
s 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.
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.
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).
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 😅
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.
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 👍🏻
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!
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:
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".
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.
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:
+194 hidden modules
「wdm」: Compiled successfully.
「wdm」: Compiling...
[wdm」: Hash: a469c61182996c46a2d3
Version: webpack 4.39.3
Time: 327ms
Built at: 10/14/2020 2:58:03 PM
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
So @adamwathan it turns out you're 100% right on this, I did some timings, and there was effectively no difference between:
@import 'tailwindcss/utilities'
utilities.css
via @import 'tailwindcss/dist/utilities.css
utilities.css
into the app.pcss
file (just to see if the postcss-import
plugin added overheadTL;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:
@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
.....
@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
.....
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
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
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.
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.
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
@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.
¯_(ツ)_/¯
Just an update here for anyone who might be reading through this issue in the future.
@apply
with no issues.@apply
works internally cause it to no longer work as described in this issue and in the article.@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
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.
@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
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.
@oliverw check my comment here: https://github.com/tailwindlabs/tailwindcss/issues/2820#issuecomment-737227525 for how to eliminate the source-maps
Kinda agreed...it's too slow as of v2. Have to wait for 10-15 seconds to see the changes.
@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 ?
@rightaway have you split up your TW files? There is a comment above with an article to speeding up your TW builds.
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.
Have you tried following approach:
null-loader
for devmodetailwind.css
for loading though index.html
before launching devmode using other config?
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 ?
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
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.
Another solution I put here https://github.com/tailwindlabs/tailwindcss/issues/2820
and that was to avoid using style-loader and try MiniCssExtractPlugin instead
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!
The latest JIT option in version 2.1 solves this issue for me. https://tailwindcss.com/docs/just-in-time-mode
@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.
@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
对于无法升级到最新版本tailwind的可以通过webpack插件解决这个问题: https://juejin.cn/post/6956424977184718856
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:
...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:
...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