-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Only expose used CSS variables #16211
Conversation
0f19b62
to
8f74b3e
Compare
c0cc98c
to
0b58b52
Compare
This PR introduces a performance improvement we noticed while working on on: #16211 We noticed that `substituteFunctions` was being called on every `node` after the `compileAstNodes` was done. However, the `compileAstNodes` is heavily cached. By moving the `substituteFunctions` we into the cached `compileAstNodes` we sped up performance for Catalyst rebuilds from ~15ms to ~10ms. | Before | After | | --- | --- | | <img width="710" alt="image" src="https://github.com/user-attachments/assets/eaf110d9-2f88-447c-9b10-c77d47bd99a5" /> | <img width="696" alt="image" src="https://github.com/user-attachments/assets/c5a2ff4c-d75e-4e35-a2b6-d896598810f5" /> |
ac2a0c9
to
cac548c
Compare
IntelliSense part of tailwindlabs/tailwindcss#16211
Hi, I'm very glad to see this is being worked on. I asked about this early May last year, but at the time this was pushed for "possible for future" which was understandable. Question regarding the implementation: does this only apply to variables in the "tailwindcss/theme" blocks?
|
@wenbei it only applies to variables defined inside This PR only improves the CSS variables, but a follow up PR is in the works to tackle The |
This currently only marks it as `used`, _if_ it's resolved via the `#var(…)` theme method.
We can only mark them as being used if the utility or variant itself is being used.
Now that we have access to the `context`, we can handle the variables tracking in the `declaration` handling itself.
This improves readability and keeps the conceptual logic separated.
We already start with a clean slate when populating `:root, :host`, so this is unnecessary work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome stuff!
integrations/cli/index.test.ts
Outdated
'src/index.css': css` | ||
@import 'tailwindcss/utilities'; | ||
@theme { | ||
--*: initial; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't really need this line in the code since you're not importing from tailwindcss
right?
ValueParser.walk(node.nodes, (child) => { | ||
if (child.kind !== 'word' || child.value[0] !== '-' || child.value[1] !== '-') return | ||
|
||
theme.markUsedVariable(child.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth verifying if the value is already trimmed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work on this! |
Very excited to see this is merged! I've been hoping this would happen for a long time. I had a custom solution which I can get rid of now! |
In tailwindcss v4.0.5, tailwindlabs/tailwindcss#16211 is smarter about not including unused theme variables, but `static` is the documented way to opt into the older behavior.
This reverts #16211 We found some unexpected interactions with using `@apply` and CSS variables in multi-root setups like CSS modules or Vue inline `<style>` blocks that were broken due to that change. We plan to re-enable this soon and include a proper fix for those scenarios. ## Test plan - Updated snapshots - Tested using the CLI in a new project: <img width="1523" alt="Screenshot 2025-02-10 at 13 08 42" src="https://github.com/user-attachments/assets/defe0858-adb3-4d61-9d2c-87166558fd68" /> --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Currently when I have |
This PR re-enables the changes necessary to remove unused theme variables and keyframes form your CSS. This change was initially landed as #16211 and then later reverted in #16403 because we found some unexpected interactions with using `@apply` and CSS variables in multi-root setups like CSS modules or Vue inline `<style>` blocks that were no longer seeing their required variables defined. This issue is fixed by now ensuring that theme variables that are defined within an `@reference "…"` boundary will still be emitted in the generated CSS when used (as this would otherwise not generate a valid stylesheet). So given the following input CSS: ```css @reference "tailwindcss"; .text-red { @apply text-red-500; } ``` We will now compile this to: ```css @layer theme { :root, :host { --text-red-500: oklch(0.637 0.237 25.331); } } .text-red { color: var(--text-red-500); } ``` This PR also improves the initial implementation to not mark theme variables as used if they are only used to define other theme variables. For example: ```css @theme { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } .default-font-family { font-family: var(--default-font-family); } ``` This would be reduced to the following now as `--font-mono` is only used to define another variable and never used outside the theme block: ```css :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --default-font-family: var(--font-sans); } .default-font-family { font-family: var(--default-font-family); } ``` ## Test plan - See updated unit and integration tests - Validated it works end-to-end by using a SvelteKit example
This PR adds documentation for the new `@theme static` option to always include all CSS variables from a theme block. Merge once tailwindlabs/tailwindcss#16211 is released. --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR only exposes used CSS variables.
My initial approach was to track the used variables, this was a bit messy because it meant that we had to walk part of the AST(s) in multiple places. We also had to be careful because sometimes if a variable exists in an AST, that doesn't mean that it's actually used. E.g.:
In this last case, the
--color-blue-500
is part of the CSS AST, but as long asfoo
the utility is not used, it won't end up in your actual CSS file, therefore the variable is not used.Alternatively, if the
foo
utility is used with an invalid variant (e.g.:group-[>.foo]:foo
, then the@utility foo
code will still run internally because variants are applied on top of the utility. This means that it looks likevar(--color-blue-500)
is being used.Another annoying side effect was that because variables are conditionally generated, that the
@theme
->:root, :host
conversion had to happen for every build, instead of once in thecompile(…)
step.To prevent all the messy rules and additional booking while walking of ASTs I thought about a different approach. We are only interested in variables that are actually used. The only way we know for sure, is right before the
toCss(…)
step. Any step before that could still throw away AST nodes.However, we do have an
optimizeAst
step right before printing to simplify and optimize the AST. So the idea was to keep all the CSS variables in the AST, and only in theoptimizeAst
step we perform a kind of mark-and-sweep algorithm where we can first check which variables are actually used (these are the ones that are left in the AST), and later we removed the ones that weren't part of known used list.Moving the logic to this step feels a natural spot for this to happen, because we are in fact optimizing the AST. We were already walking the AST, so we can just handle these cases while we are walking without additional walks. Last but not least, this also means that there is only a single spot where need to track and remove variables.
Now, there is a different part to this story. If you use a variable in JS land for example, we also want to make sure that we keep the CSS variable in the CSS. To do this, we can mark variables as being used in the internal
Theme
.The Oxide scanner will also emit used variables that it can find such as
var(--color-red-500)
and will emit--color-red-500
as a "candidate". We can then proactively mark this one as used even though it may not be used anyway in the actual AST.Always including all variables
Some users might make heavy use of JavaScript and string interpolation where they need all the variables to be present. Similar to the
inline
andreference
theme options, this also exposes a newstatic
option. This ensures that all the CSS variables will always be generated regardless of whether it's used or not.One handy feature is that you have granular control over this:
Performance considerations:
Now that we are tracking which variables are being used, it means that we will produce a smaller CSS file, but we are also doing more work (the mark-and-sweep part). That said, ran some benchmarks and the changes look like this:
Running it on Catalyst:

(probably within margin of error)
Running it on Tailwind UI:

Test plan
The diff on Catalyst looks like this:
If you have
ripgrep
installed, you can use this command to verify that these variables are indeed not used anywhere:The only exception I noticed is that we have this:
But this is not a variable, but it's replaced at build time with the actual value, so this is not a real issue.
Testing on other templates:
Fixes: #16145