fix(sui-critical-css): carry @property, @font-face and referenced @keyframes into the rebuilt CSS - #1997
Merged
Conversation
…e rebuilt CSS Chrome's CSS coverage reports ranges that cover style rules only. 1.34.0 used that to restore the at-rules a covered rule is NESTED IN, but an at-rule that defines something instead of styling something matches no element, so it is never attributable to a range and no amount of nesting context brings it back. Measured on a Tailwind v4 app: the rebuilt CSS had 0 `@property`, 0 `@font-face` and 0 `@keyframes` against 29, 5 and 30 in the source sheet. `@property` is the one that changes what the browser paints. With the registration missing, `border-style: var(--tw-border-style)` resolves to the empty token, which is invalid at computed-value time, so `border-style` computes to its initial `none` — and with `border-style: none` the computed `border-width` is `0px` even though the utility's `1px` applied and won the cascade. Every button styled that way renders with no border until the full stylesheet arrives, and since consumers make the real sheets async whenever the critical CSS is non-empty, that is a visible jump rather than a progressive paint. So: - `@property` and `@font-face` are always kept, the same exception `@layer` statements already have. Both are cheap and safe when unused: a registration only sets an initial value, and a `@font-face` never triggers a download unless a matched rule asks for the family. - `@keyframes` is not bounded in size, so it is only kept when a declaration that survived the rebuild names it (`animation` or `animation-name`, vendor prefixes included). Scanning only the DIRECT declarations of the kept nodes, so a kept `@media` does not drag in the keyframes named by the children that were discarded from it. Measured on the same 425 KB production sheet with the coverage of 221 rules: 33279 -> 36913 bytes unminified (+3.6 KB), 29 of 29 `@property`, 5 of 5 `@font-face`, and 1 of 30 `@keyframes` kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomasmax
requested review from
andresin87,
andresz1,
ferransimon,
kikoruiz and
sui-bot
as code owners
September 28, 2026 14:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Chrome's CSS coverage reports ranges that cover style rules only. 1.34.0 used that fact to restore the at-rules a covered rule is nested in, which fixed the flattening of
@media/@layer/@supports. It does nothing for an at-rule that defines something instead of styling something:@property,@font-faceand@keyframesmatch no element, so coverage never reports a range for them and no amount of nesting context brings them back.Measured on a 425 KB Tailwind v4 production sheet, rebuilding it with the coverage of 221 rules:
master@property@font-face@keyframesThe
@propertygap changes what the browser paints. Tailwind v4 emitsborder-style: var(--tw-border-style)in its preflight and relies on@property --tw-border-stylefor the initial value. Without the registration thevar()resolves to the empty token, which is invalid at computed-value time, soborder-stylecomputes to its initialnone— and withborder-style: nonethe computedborder-widthis0pxeven though the utility's1pxapplied and won the cascade. Every button styled that way renders with no border until the full stylesheet arrives. Because consumers make the real sheets async (media="print"+onload) as soon as the critical CSS is non-empty, that is a visible jump, not a progressive paint. Injecting nothing but@property --tw-border-styleinto the served critical CSS restores the 1px and the button's height.Three
--tw-*custom properties were referenced by the critical CSS and defined nowhere in it:--tw-border-style,--tw-ease,--tw-outline-style.What this does
@propertyand@font-faceare always kept, which is the same exception@layerstatements already have and for the same reason: the coverage cannot mark them, and they are the only thing that makes the rules depending on them behave. Both are cheap and safe to keep when unused — a registration only sets an initial value, and a@font-facenever triggers a download unless a matched rule asks for the family.@keyframesis only kept when a declaration that survived the rebuild names it (animationoranimation-name, vendor prefixes included). Unlike the other two it is not bounded in size, so keeping all of them unconditionally would be real bloat. Only the direct declarations of the kept nodes are scanned, so a kept@mediadoes not drag in the keyframes named by the children that were discarded from it.Cost
Same sheet and same coverage as the table above:
master@property@font-face@keyframes+3.6 KB unminified, and 29 of the 30
@keyframesare still dropped.Tests
8 new specs in
test/server/covered-cssSpec.js(38 passing): a@propertykept with nothing else covering it, a@propertynested in@layer properties, an uncovered@font-face,@keyframesreached throughanimation/animation-name/ a vendor prefix, a@keyframesnobody names, and a@keyframesnamed only by a rule that was discarded.Known limitation, out of scope here
Tailwind's fallback for browsers without
@propertysupport is a@layer properties { @supports (…) { *, ::before, … { --tw-*: initial } } }block whose@supportscondition is false in Chromium. A style rule inside a false@supportscan never be covered, so that block still cannot be carried over and Safari < 16.4 stays exposed. Keeping it would mean keeping rules on a "this at-rule is a polyfill" heuristic rather than on coverage, which is a different discussion.🤖 Generated with Claude Code