Closed bjornharvold closed 10 months ago
Hi @bjornharvold,
Thanks for the issue. It's worth mentioning that TailwindCSS requires postcss which does force a slow path for CSS processing. As the application grows this will become more and more noticeable.
That said, to undertstand a bit better the issue and that you are using the optimal Tailwinds settings, can you please share your tailwind configuration?
Hi @alan-agius4
Our shared config looks like this:
function withOpacityValue(variable) {
return ({ opacityValue }) => {
if (opacityValue === undefined) {
return `rgb(var(${variable}))`;
}
return `rgb(var(${variable}) / ${opacityValue})`;
};
}
/** @type {import('tailwindcss').Config} */
module.exports = {
prefix: 'wink-',
important: true,
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
primary: withOpacityValue('--wink-color-primary'),
secondary: withOpacityValue('--wink-color-secondary'),
success: withOpacityValue('--wink-color-success'),
danger: withOpacityValue('--wink-color-danger'),
warning: withOpacityValue('--wink-color-warning'),
info: withOpacityValue('--wink-color-info'),
light: withOpacityValue('--wink-color-light'),
dark: withOpacityValue('--wink-color-dark'),
body: withOpacityValue('--wink-color-body'),
muted: withOpacityValue('--wink-color-muted'),
white: withOpacityValue('--wink-color-white'),
},
extend: {
},
},
corePlugins: {
aspectRatio: true,
},
plugins: [
require('@tailwindcss/forms'),
// require('@tailwindcss/aspect-ratio'),
],
};
Our styles.css has 3 Tailwind includes:
@tailwind base;
@tailwind components;
@tailwind utilities;
All our standalone components have only 2:
@tailwind components;
@tailwind utilities;
and their Tailwind config refers back to the shared presets:
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
const { join } = require('path');
const sharedTailwindConfig = require('../../../libs/tailwind-preset/tailwind-config');
module.exports = {
presets: [sharedTailwindConfig],
content: [
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
...createGlobPatternsForDependencies(__dirname),
],
};
I dug a bit into this and there are 2 potential issues.
The paths in the content
will return pretty much all the files of the application, thus causing any of the already stylesheet to be invalidated upon any change. This is because any of matching files will be marked as a direct dependency of the CSS stylesheet. This fundamentally is not an issue that we can fix from our end as it's how tailwinds is configured to work.
For every component style, we create a new instance of the PostCSS which in turns create a new instance of tailwind, which causes the a large number of FS calls to return the find the files matching the globs. This is something that we should be able to handle.
Regarding 1. Nx's incremental builds only requires recompiling components that come between the changed component and the app (top-most layer). Wondering if Angular can do something similar with its cache. I am guessing Angular does something similar, becase, as I mentioned, a re-build only takes 50% of the time compared to the starting the app. That sounds good but when the startup is 10 mins and every rebuild is 5 mins... not a lot is able to get done in one day π¬
Also, is it worth reaching out to the guys at Tailwind for 1 at all?
Cheers
What you are describing is actually how the CLI's rebuilds work in watch mode.
Unfortunately, the issue here is that the Tailwind configuration is causing Tailwind to notify the build system that all files within the contents
option will affect all stylesheets with Tailwind usage. This means that any change to a file referenced in contents
will cause all those stylesheets to be rebuilt since they could change as a result. If any of those stylesheets are used by a component then the component itself may need to be changed so it will get rebuilt. Effectively, there are no longer any layers. Rather, the majority of the application's files are now dependent on each other and each rebuild is a nearly full rebuild.
Wondering if that can somehow be "bypassed" when in serve mode. The esbuild / TailwindCSS story works great. For regular development, I would be fine with PostCSS working once at startup but not for a re-build. I say that because designing the UI with TailwindCSS can easily be done with Storybook. There are mostly Typescript & logic changes in HTML occurring in dev mode during a rebuild; not the changing of CSS. Just an idea π€
Tailwind requires PostCSS to run, hence it always needed to run for every change.
In this case, your tailwind configuration is set so that every file in your application is a dependency of every stylesheet. As such in this case the CLI is doing the right thing, that once a any file is changed change it is invalidating all the stylesheets.
In the case you want a different behaviour, you can provide different content
based on an environment variable or parameter, but this is not a problem that the Angular CLI should be solving.
An alternative way, would also be to use multiple configurations where with this approach you can limit the dependency scope and fine grain the dependency structure for each stylesheet.
17.0.3 which was released yesterday contains a performance improvement to only create post css once.
PR: https://github.com/angular/angular-cli/pull/26391
Can you check if this improves the performance for your case? Other than that, I donβt think there is much else we can do to improve the performance for tailwinds for your current configuration.
Hi @alan-agius4
The PR improved the rebuild time. From 290s to 169s for one of our apps. π₯
Regarding multiple configs...
We have 500+ components. Every component uses the same Tailwind config in its Angular module library so we have 500 identical Tailwind config files like so:
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
const { join } = require('path');
const sharedTailwindConfig = require('../../../libs/tailwind-preset/tailwind-config');
module.exports = {
presets: [sharedTailwindConfig],
content: [
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
...createGlobPatternsForDependencies(__dirname),
],
};
Not sure how using @config in any of the 500+ components would affect change detection. Currently, a change in one of the 500 components triggers a rebuild for everything. This is where Nx's incremental build saved us to some extent as it completely ignored rebuilding anything below the change tree.
Wondering if there is anything we can do in these 500+ individual but identical Tailwind configs that would stop untouched components from re-building.
Glad to hear that the performance fix reduced your build times by around 40%.
Having a single tailwinds config is not really scalable when using tailwinds in components. This is because dependencies are not statically inferred instead these needs to be provided through the content
option.
Since you have 500 components, you get 500 different styles compilation each of these stylesheets dependent on a very broad content
settings which configures the entire application to be a dependency of each stylesheet, this is a what is causing the slow incremental compilation, as at the very minimum even for a trivial change like changing a comment, there are 500 stylesheets that require to be compiled because there are invalidated. Which is all expected and intended from a tooling POV, as that is that the tool has been configured to do.
A more performant way configure tailwind is that each feature, has it's own list of dependencies. Consider the below structure and files.
βββ src
β βββ app
β β βββ app.component.scss // This sets `@config './app.component.config.js'
β β βββ app.component.ts
β β βββ app.component.config.js
β βββ home
β β βββ home.component.scss // This sets `@config './home.component.config.js'
β β βββ home.component.ts
β β βββ home.component.config.js
β βββ styles.scss
βββ tailwind.config.js
βββ tailwind.shared.config.js
tailwind..shared.config.js
function withOpacityValue(variable) {
return ({ opacityValue }) => {
if (opacityValue === undefined) {
return `rgb(var(${variable}))`;
}
return `rgb(var(${variable}) / ${opacityValue})`;
};
}
/** @type {import('tailwindcss').Config} */
module.exports = {
prefix: 'wink-',
important: true,
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
primary: withOpacityValue('--wink-color-primary'),
secondary: withOpacityValue('--wink-color-secondary'),
success: withOpacityValue('--wink-color-success'),
danger: withOpacityValue('--wink-color-danger'),
warning: withOpacityValue('--wink-color-warning'),
info: withOpacityValue('--wink-color-info'),
light: withOpacityValue('--wink-color-light'),
dark: withOpacityValue('--wink-color-dark'),
body: withOpacityValue('--wink-color-body'),
muted: withOpacityValue('--wink-color-muted'),
white: withOpacityValue('--wink-color-white'),
},
extend: {
},
},
corePlugins: {
aspectRatio: true,
},
plugins: [
require('@tailwindcss/forms'),
// require('@tailwindcss/aspect-ratio'),
],
};
tailwind.config.js
const { join } = require('path');
const sharedTailwindConfig = require('./tailwind..shared.config.js');
module.exports = {
presets: [sharedTailwindConfig],
content: [
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
],
};
<app|home>.component.config.js
const { join } = require('path');
const sharedTailwindConfig = require('./tailwind..shared.config.js');
module.exports = {
presets: [sharedTailwindConfig],
content: [
join(__dirname, '**/!(*.stories|*.spec).{ts,html}'),
],
};
In the above example, the app.component.scss
will only get invalidated when something under the app
directory is modified or other direct dependencies which can be inferred during statically during the build. This is because in app.component.config.js
the content
option has a different values for each component stylesheet.
Hi @alan-agius4
Trying that out. What happens with tailwind.config.js in your setup? Looks like it's not being used. Or, correct me if I am wrong, it just needs to be in the root of the SPA app to be picked up by the pre-processor?
Should I remove tailwindConfig completely from the project.json component libraries (assuming this is an Nx-specific setting) and instead move each individual file to the same directory as my .css file?
Excited to see how this performs πΊ
OMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMG π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯
@alan-agius4 You singlehandedly brought our ng serve time down from 596s to 18s with this change. Rebuild time went from 169s to 6s (on certain re-builds < 1s). You made working with TailwindCSS FUN again π¦ΈββοΈππ¦ΈββοΈππ¦ΈββοΈππ¦ΈββοΈπ
πββοΈπββοΈπββοΈ THANK YOU!!!!! πββοΈπββοΈπββοΈ
π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯ OMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMGOMG
@bjornharvold, super glad to hear that this made such as big improvements for your codebase and DX.
The root level tsconfig is used for the global stylesheets and also I am not 100% sure if it is used by tailwindcss to extend the @config
provided from the root directory
This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.
Command
serve
Description
When using the new esbuild feature from Angular 17.0.3 and TailwindCss, it takes us 500+ seconds to start up a large application. When we make a simple code change, it takes 280+ seconds for esbuild to rebuild.
The esbuild build story is GREAT. Working with esbuild and TailwindCSS on a daily basis is a completely different story.
Wondering what's possible here. The current situation is not tenable.
FYI Before we used Nx's incremental webpack build and that worked in an "acceptable" way. If the change was in the "top most" part of the application, a rebuild was snappy. If the change was made in the "lower part" of the app, the dev server would have to work its way up to the top by compiling everything in between.
You definitely see a delay when having to pre-process the CSS in a larger application in comparison to an Angular app that runs with SCSS / Bootstrap for example.
Thoughts from the Team Angular highly requested πΊ
Describe the solution you'd like
@vbraun said that it works fine for him with a static SCSS TailwindCSS solution here: https://github.com/angular/angular-cli/issues/25130#issuecomment-1814106769 but that the CSS doesn't get pruned.
Wondering if we can have TailwindCSS classes be configured to be pruned on a production build but use the entire TailwindCSS library during development (serve).
If we can define a development configuration that sucks in TailwindCSS in its entirety and a build configuration that kicks off post-processing of TailwindCSS in standalone component libraries and does the pruning, we might have ourselves a working solution.
Describe alternatives you've considered
No response