Skip to content

perf(css): write selectors, An+B and more values in their shortest spelling - #21664

Merged
alexander-akait merged 14 commits into
mainfrom
perf/css-minify-structural
Aug 11, 2026
Merged

alexander-akait merged 14 commits into
mainfrom
perf/css-minify-structural

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Aug 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Runs cssnano's (1,711) and lightningcss's (1,519) own minification assertions against webpack's printer and closes every gap that is both shorter and provably equivalent, plus one defect the comparison exposed: _wouldFuseTokens treated any + before . as starting a number, so .a+.m printed as .a+/**/.m — longer than its input, and different again on a second pass.

A selector list is now written as the set it is (one order, no repeats), An+B in its shortest spelling with :nth-child(1) naming :first-child, matrix3d reduced to matrix where the 3D part is the identity, the implied universal dropped inside :not()/:is(), a zero time carrying the shorter of its two units, and a component spelling its property's own initial dropped. Every rewrite was adjudicated in headless Chromium before it shipped; several that cssnano and lightningcss perform were rejected there and are recorded as declined instead.

yarn test:size moves 15 of the configCases assets, all downward: −1,453 raw / −514 gzip across an 18-stylesheet framework corpus (Bootstrap, Tailwind, Bulma, Foundation, Material, Semantic UI, Pico, Tachyons, UIkit, Font Awesome, …), with no class selector lost by any fixture.

Two commits are pure refactor and change no output: the An+B named equivalents and the merge-table longhand union are now derived in tooling/generate-css-data.js rather than typed into the printer, and the hot paths allocate less — a rule's declarations are indexed once for all three merge tables instead of three times (48,204 Map/Set allocations and ~401,700 closures removed per corpus pass), the tables are skipped entirely for the 78% of candidate rules that cannot match one (311,900 fewer entry checks), and a selector list is only built for the 8.4% of preludes that hold a comma.

What kind of change does this PR introduce?

perf — with one fix (the empty comment after a sibling combinator) and two output-neutral refactors.

Did you add tests for your changes?

Yes — test/CssSyntax.unittest.js and test/CssValueSyntax.unittest.js (unit, including the new generator collectors), and test/configCases/css/minimize-selectors/ and test/configCases/css/minimize-values/ (integration, each new case paired with the declined one next to it). test/SyntaxBrowserEquivalence.unittest.js reads a selector list and an An+B as what they select rather than how they are written, which is the invariant the rewrites rest on; I mutation-tested that change — dropping a selector, flipping odd to even, and shifting the An+B lift each make it fail.

Does this PR introduce a breaking change?

No. Every rewrite is equivalent to the engine; ConfigTestCases and ConfigCacheTestCases pass unchanged apart from the snapshots of the cases above.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a — no option or public API changes. The transforms webpack declines, and why, are documented next to them in lib/css/cssMinify.js and tooling/generate-css-data.js.

Use of AI

Yes. Claude Code was used throughout: to extract and replay the upstream cssnano and lightningcss assertions against webpack's printer, to write the implementation and tests, and to run the measurements quoted above. Every claimed equivalence was checked in headless Chromium rather than assumed, and the ones that failed there were dropped — transition family merging (it clobbers an earlier transition-behavior), translate(var(--x),0) reduction (it revives a declaration the browser drops), background:none → 0 0, and 0% → 0 on background-position. All output and measurements were reviewed by me before pushing.


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved CSS minification for An+B selectors, selector lists, and selector functions.
    • Corrected zero-time handling, unit conversion, numeric token boundaries, and shorthand reductions.
    • Preserved meaningful behavior when simplifying selectors and transforms.
  • Performance & Output Improvements

    • Reduced CSS size through selector deduplication, canonical ordering, transform simplification, and keyframe percentage conversion.
    • Simplified eligible initial-value keywords and equivalent 2D transforms.
    • Improved declaration merging and normalization of equivalent CSS rules.

@changeset-bot

changeset-bot Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bb98b3b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added area: css CSS support (lib/css) area: tooling Repo tooling, codegen, CLI entry (tooling/, bin/) labels Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@alexander-akait, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ce95da5f-f1aa-4660-8d8c-d12532fa5473

📥 Commits

Reviewing files that changed from the base of the PR and between 78b8574 and bb98b3b.

📒 Files selected for processing (3)
  • lib/css/syntax.js
  • test/CssValueSyntax.unittest.js
  • tooling/generate-css-data.js

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "tools"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

The CSS minifier now normalizes additional An+B selectors, selector lists, keyframe positions, zero values, transforms, and shorthand declarations. Generated CSS metadata supports these changes. Equivalence tests compare canonical selector sets and grouped declaration rules.

Changes

CSS minification normalization

Layer / File(s) Summary
Generated CSS metadata
tooling/generate-css-data.js, test/CssValueSyntax.unittest.js, cspell.json
The generator derives and exports nth-selector equivalent and omittable-initial-keyword tables. Tests cover the generated mappings.
Selector and An+B normalization
lib/css/syntax.js, test/CssSyntax.unittest.js, test/configCases/css/minimize-selectors/style.css
Selector processing normalizes An+B expressions, canonicalizes selector lists, removes implied universal selectors, preserves sibling combinators, and converts equivalent keyframe positions.
Value and declaration minification
lib/css/syntax.js, test/CssSyntax.unittest.js, test/configCases/css/minimize-values/style.css
Value minification handles zero-time conversion and additional transform reductions. Declaration minification omits eligible initial shorthand keywords and validates longhand merging.
CSS equivalence comparison
test/SyntaxBrowserEquivalence.unittest.js, .changeset/031-css-selector-sets-and-transforms.md
Equivalence comparison canonicalizes selector formulas and selector sets, groups selectors by declaration blocks, and preserves rule ordering semantics. The changeset records a patch release.

Possibly related PRs

  • webpack/webpack#21648: Both changes modify CSS minification logic in lib/css/syntax.js, including shorthand and transform reduction behavior.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title uses valid Conventional Commit syntax and accurately describes the CSS minification changes, but the branch prefix is not provided for verification. Provide the branch name or prefix to verify that the perf type matches the required branch prefix.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (4015bbd).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@4015bbd
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@4015bbd
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@4015bbd

@codecov

codecov Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.52%. Comparing base (2d8678a) to head (bb98b3b).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21664      +/-   ##
==========================================
- Coverage   94.52%   94.52%   -0.01%     
==========================================
  Files         630      630              
  Lines       82008    82131     +123     
  Branches    24272    24328      +56     
==========================================
+ Hits        77520    77634     +114     
- Misses       4488     4497       +9     
Flag Coverage Δ
css-parsing 26.57% <12.58%> (-0.02%) ⬇️
html5lib 30.99% <ø> (ø)
integration 88.23% <33.77%> (-0.11%) ⬇️
test262 51.78% <ø> (ø)
unit 54.58% <100.00%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Generated code size

Comparing bb98b3b merged into 2d8678a against 2d8678a. Merging this PR will change the code webpack generates.

Changed New Deleted Unchanged Raw change
Cases 5 0 0 1549 🔴 ↑ +2.13 KiB
Assets 7 0 0 6035 🔴 ↑ +2.13 KiB
Runtimes 0 0 0 1898 —
7 asset(s) changed size
Asset Before After Change Gzip (9) Brotli (11) Zstd (19)
🔴 ↑ css/minimize-selectors bundle0.css 6.33 KiB 7.92 KiB +1.58 KiB (+24.94%) +26.40% +25.56% +25.84%
🔴 ↑ css/minimize-values bundle0.css 17.48 KiB 18.15 KiB +689 B (+3.85%) +3.93% +4.05% +3.84%
🟢 ↓ css/css-modules web-production/652.bundle1.css 26.61 KiB 26.58 KiB -29 B (-0.11%) +0.04% -0.04% —
🟢 ↓ css/css-modules web-production-global/244.bundle5.css 29.23 KiB 29.20 KiB -29 B (-0.10%) +0.04% +0.31% +0.01%
🟢 ↓ css/large 1/961.bundle1.css 1.28 MiB 1.28 MiB -24 B (-0.00%) -0.00% +0.01% +0.08%
🟢 ↓ css/large 1/382.bundle1.css 1.87 MiB 1.87 MiB -24 B (-0.00%) -0.00% +0.05% -0.03%
🟢 ↓ css/minimize-lightningcss-selectors bundle0.css 13.48 KiB 13.47 KiB -16 B (-0.12%) +0.10% +0.23% +0.11%

No runtime gained or lost a runtime module.

Built test/configCases with the defaults a user gets: 1554 case(s), 6042 asset(s), 40 emitted nothing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
test/SyntaxBrowserEquivalence.unittest.js (1)

384-388: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the new prose comments to two short lines or fewer.

  • test/SyntaxBrowserEquivalence.unittest.js#L384-L388: shorten the An+B explanation.
  • test/SyntaxBrowserEquivalence.unittest.js#L441-L445: shorten the selector-set explanation.
  • test/SyntaxBrowserEquivalence.unittest.js#L1162-L1165: shorten the selector-order explanation.
  • test/SyntaxBrowserEquivalence.unittest.js#L1211-L1213: shorten the rule-grouping explanation.

As per coding guidelines, comments must be “generally one line and no more than two short lines.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/SyntaxBrowserEquivalence.unittest.js` around lines 384 - 388, Shorten
the four explanatory comments in test/SyntaxBrowserEquivalence.unittest.js at
lines 384-388, 441-445, 1162-1165, and 1211-1213 to two short lines or fewer,
preserving only the essential explanation of the An+B, selector-set,
selector-order, and rule-grouping behavior.

Source: Coding guidelines

lib/css/syntax.js (1)

7901-7929: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Module-level _mergeAt and _mergeRepeated couple _indexDeclarations to its caller by ordering alone.

The pair is safe today: line 9020 sets mergeable only when the rule has no child rules, so no nested rule is printed between _indexDeclarations and the _mergeBoxLonghands calls that read the state. The invariant is invisible at the definition site.

Consider returning the index from _indexDeclarations and passing it into _mergeBoxLonghands, or record the node the index belongs to and assert it on read. The allocation the comment avoids is one small object per rule, not one per table entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/css/syntax.js` around lines 7901 - 7929, Decouple _indexDeclarations from
its caller’s ordering by returning the per-rule merge state and passing it
explicitly into _mergeBoxLonghands, or associate the module-level state with its
indexed rule and validate that association when reading it. Update the related
merge call sites while preserving the existing candidate and repeated-property
behavior.
tooling/generate-css-data.js (1)

2382-2406: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert every omittableInitialKeywords entry is collected

If a future mdn-data update changes grid-auto-flow.initial or its syntax shape, collectOmittableInitialKeywords silently omits it. Assert that every supplement name yields an output entry, similar to assertPrimitivesExist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/generate-css-data.js` around lines 2382 - 2406, Update
collectOmittableInitialKeywords to assert that every name in
SUPPLEMENT.omittableInitialKeywords produces an entry in the collected output,
rather than silently continuing when property metadata, initial value, or syntax
shape is incompatible. Follow the assertion pattern used by
assertPrimitivesExist and retain the existing collection and sorting behavior
for valid entries.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/css/syntax.js`:
- Around line 6600-6611: The _minifyNth function must avoid hanging or losing
precision for unsafe B values: return null when a or b is not a safe integer,
and replace the positive-step normalization loop with the specified modulo-based
normalization. In test/CssSyntax.unittest.js lines 1409-1416, add coverage
asserting min("a:nth-child(2n-99999999999999999999){b:c}") remains unchanged.

In `@test/CssSyntax.unittest.js`:
- Around line 1409-1416: Extend the existing min() test in “keeps an An+B
selector argument no shorter spelling reaches” with an :nth-child() case whose B
magnitude exceeds Number.MAX_SAFE_INTEGER, asserting the input remains
unchanged. Use a valid large An+B expression so the regression test exercises
_minifyNth’s previously unbounded normalization loop.

In `@test/SyntaxBrowserEquivalence.unittest.js`:
- Around line 1107-1120: The nested style condition handling in conditionOf must
apply the same oneSpelling selector canonicalization as top-level labels before
sorting and joining the selector list. Update sortedSelectorList to canonicalize
each selector with oneSpelling, preserving deduplication and sorting so
equivalent An+B and pseudo-class forms produce identical chain keys.
- Around line 403-439: Update oneSpelling so NTH_CALL normalization only
processes unescaped :nth-* functional pseudo-classes in selector syntax, not
text inside quoted strings or attribute selectors. Parse or scan selector tokens
while tracking strings and attribute-value context, and apply the existing An+B
normalization only to eligible pseudo-class tokens. Preserve all literal
attribute and string contents unchanged.

In `@tooling/generate-css-data.js`:
- Around line 3619-3628: Move the emitted MERGE_LONGHANDS and
OMITTABLE_INITIAL_KEYWORDS constants to after the FAMILY_LONGHANDS block so the
PAIR_LONGHANDS comment remains directly attached to its subject. Update the
OMITTABLE_INITIAL_KEYWORDS comment to explicitly name BOX_LONGHANDS,
FAMILY_LONGHANDS, and PAIR_LONGHANDS instead of referring to tables “above.”

---

Nitpick comments:
In `@lib/css/syntax.js`:
- Around line 7901-7929: Decouple _indexDeclarations from its caller’s ordering
by returning the per-rule merge state and passing it explicitly into
_mergeBoxLonghands, or associate the module-level state with its indexed rule
and validate that association when reading it. Update the related merge call
sites while preserving the existing candidate and repeated-property behavior.

In `@test/SyntaxBrowserEquivalence.unittest.js`:
- Around line 384-388: Shorten the four explanatory comments in
test/SyntaxBrowserEquivalence.unittest.js at lines 384-388, 441-445, 1162-1165,
and 1211-1213 to two short lines or fewer, preserving only the essential
explanation of the An+B, selector-set, selector-order, and rule-grouping
behavior.

In `@tooling/generate-css-data.js`:
- Around line 2382-2406: Update collectOmittableInitialKeywords to assert that
every name in SUPPLEMENT.omittableInitialKeywords produces an entry in the
collected output, rather than silently continuing when property metadata,
initial value, or syntax shape is incompatible. Follow the assertion pattern
used by assertPrimitivesExist and retain the existing collection and sorting
behavior for valid entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d53980-f0f5-4191-b075-24ef86d83f99

📥 Commits

Reviewing files that changed from the base of the PR and between 78d0d4c and e0afdec.

⛔ Files ignored due to path filters (7)
  • lib/css/data.js is excluded by !lib/css/data.js
  • test/configCases/css/minimize-lightningcss-selectors/__snapshots__/ConfigCacheTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
  • test/configCases/css/minimize-lightningcss-selectors/__snapshots__/ConfigTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
  • test/configCases/css/minimize-selectors/__snapshots__/ConfigCacheTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
  • test/configCases/css/minimize-selectors/__snapshots__/ConfigTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
  • test/configCases/css/minimize-values/__snapshots__/ConfigCacheTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
  • test/configCases/css/minimize-values/__snapshots__/ConfigTest.snap is excluded by !**/*.snap, !test/**/__snapshots__/**
📒 Files selected for processing (8)
  • .changeset/031-css-selector-sets-and-transforms.md
  • lib/css/syntax.js
  • test/CssSyntax.unittest.js
  • test/CssValueSyntax.unittest.js
  • test/SyntaxBrowserEquivalence.unittest.js
  • test/configCases/css/minimize-selectors/style.css
  • test/configCases/css/minimize-values/style.css
  • tooling/generate-css-data.js
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • webpack/schema-utils (auto-detected)
  • webpack/tapable (auto-detected)

Comment thread lib/css/syntax.js Resolved
Comment thread test/CssSyntax.unittest.js Resolved
Comment thread tooling/generate-css-data.js Resolved
@codspeed

codspeed Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 3 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 8 improved benchmarks
❌ 3 regressed benchmarks
✅ 299 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory unit benchmark "html-printer-document-unit", walk (fragment, tr context) 6.6 KB 19.7 KB -66.59%
❌ Memory benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 236.6 KB 389.4 KB -39.25%
❌ Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 862.2 KB 1,156.6 KB -25.45%
⚡ Memory benchmark "many-modules-esm", scenario '{"name":"mode-development","mode":"development"}' 2,298.5 KB 818 KB ×2.8
⚡ Simulation unit benchmark "css-printer-tailwind-unit", minify (buffered blocks) 1.9 s 1.3 s +53.18%
⚡ Simulation unit benchmark "css-printer-tailwind-unit", minify (streamed block) 2 s 1.3 s +51.24%
⚡ Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1,145.2 KB 793.2 KB +44.37%
⚡ Simulation unit benchmark "css-printer-tailwind-unit", minify (retracted declarations) 2 s 1.4 s +42.25%
⚡ Simulation unit benchmark "css-printer-tailwind-unit", minify (flat rules) 3 s 2.3 s +33.74%
⚡ Simulation unit benchmark "css-printer-tailwind-unit", minify (tailwind) 5.3 s 4 s +30.79%
⚡ Memory benchmark "future-defaults", scenario '{"name":"mode-development","mode":"development"}' 1,027.6 KB 817.9 KB +25.64%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/css-minify-structural (bb98b3b) with main (8e01cb1)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (2d8678a) during the generation of this report, so 8e01cb1 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

`scale()` with a factor of 1 on one axis is that axis's own function, and a
translation's zero component is zero in either length or percentage. A keyframe
selector of 100% is the `to` CSS Animations 1 §4 names it.
A selector list is a set: the same selectors in any order match the same
elements at the same specificity. Writing one canonical order lets equal lists
compress alike and lets two rules naming one set join, and a repeat in it
matches nothing the first already did.

The equivalence harness compares a run of selectors reaching the same block
under the same conditions as the set it is, which is the invariant joining
them into one list already rests on.
CSS Syntax 3 §4.3.10 starts a number at `+` only before a digit, or before a
`.` that itself has one. Treating every `+.` as a number made `.a+.m` print as
`.a+/**/.m`, longer than the input it came from and different again on a second
pass. A join also drops a selector the other side already names.
…spelling

`An+B` carries its own whitespace, signs and case, an index under 1 matches
nothing so a forward step starts where it does, and a step selecting exactly one
child is the child Selectors 4 §14 names. A 3D matrix whose third row and column
are the identity's is the 2D matrix of the six values it leaves, and a 2D pair of
1 leaves the z scale. A zero time keeps a unit where a zero length drops one, so
it takes the shorter of the two it can carry.

The equivalence harness reads each An+B as the sequence it selects: the engine
folds `even` / `odd` itself but hands `0n+3` back as written.
A selector function's argument is a selector like the prelude's, so the
universal a compound implies says nothing there either — `:not(*.g)` is
`:not(.g)`. A lone universal is the selector, a parted one is a combinator
away, and a namespaced one is not implied.
…ly when it is one

The first/last counterpart of each An+B pseudo-class was a table typed into the
printer; it is now read out of the selector table in the generator — the An+B
half by its grammar, the counterpart by the name the pair builds, kept only
where the table has it as a selector too.

The two selector-list helpers were one function apart, and both split every
prelude to find out there was nothing to do: 50,434 of the corpus's 55,055 hold
no comma at all, so a list is only built where one can exist. `scale` and
`rotate3d` asked whether a component was one in two different spellings; both
now use the same test, which also reads `1.0`.
Every rule with two declarations was offered to three shorthand tables, and each
built the same property index again: 48,204 Map and Set allocations across the
corpus where one reused pair does. The table scan asked for its longhands
through a closure per entry — 401,700 of them — which is now a loop. And
`_isJoinablePrelude` split every prelude into a list to walk it, where 50,434 of
the corpus's 55,055 hold one selector and need no list at all.

Counted, not timed: the output is byte-identical on all 18 fixtures, and the
wall-clock difference on this machine is inside its own run-to-run spread.
`grid-auto-flow:row dense` is the `dense` omitting its group already means. The
keyword is read out of the property table and kept only where it really is the
initial and really is one alternative of a top-level `||` group; which of those
groups falls back to it is the judgement stated in the generator, since
`aspect-ratio:auto 3/2` is a ratio with a fallback rather than the ratio alone.

A rule is also offered to the three merge tables only when two of its properties
are longhands one of them names — 6,238 of the corpus's 8,034 candidate rules
are not, which is 311,900 table-entry checks that no longer run. The union is
built in the generator out of the very arrays it emits, so it cannot drift.
`_minifyNth` read an unbounded digit run, so a `B` above
`Number.MAX_SAFE_INTEGER` made `b += a` a fixed point and the
normalization loop never exited. Guard both terms and normalize by
modulo instead of by stepping.

Also spell the two new words for cspell, keep the `PAIR_LONGHANDS`
comment with its table, assert the omittable initial keyword is still
one mdn-data states, and canonicalize a nested rule's selector list in
the equivalence harness without rewriting text inside an attribute.
Deduplicating across the seam re-read the whole accumulated selector list
once per rule joined, so a run of adjacent rules printing the same block
was quadratic: 6000 of them took 2.8s where the base took 0.10s. It
bought nothing measurable — no configCases stylesheet loses a byte
without it — so the seam concatenates and the canonical form stays where
the list is the author's own.
@alexander-akait
alexander-akait force-pushed the perf/css-minify-structural branch from bad274d to 78b8574 Compare August 11, 2026 08:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tooling/generate-css-data.js`:
- Around line 2360-2366: Shorten the newly added comments at the documented
locations to no more than two short lines, retaining only non-obvious safety or
derivation constraints. Preserve an explicit brief reason why the grid-auto-flow
SUPPLEMENT entry cannot be derived, and apply the same concise style to the
comments near the An+B pseudo-class and other referenced symbols.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d94861f-3f60-42e0-8443-e9cf5504f9af

📥 Commits

Reviewing files that changed from the base of the PR and between bad274d and 78b8574.

⛔ Files ignored due to path filters (1)
  • lib/css/data.js is excluded by !lib/css/data.js
📒 Files selected for processing (5)
  • cspell.json
  • lib/css/syntax.js
  • test/CssSyntax.unittest.js
  • test/SyntaxBrowserEquivalence.unittest.js
  • tooling/generate-css-data.js
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • webpack/schema-utils (auto-detected)
  • webpack/tapable (auto-detected)
🚧 Files skipped from review as they are similar to previous changes (4)
  • cspell.json
  • test/SyntaxBrowserEquivalence.unittest.js
  • test/CssSyntax.unittest.js
  • lib/css/syntax.js

Comment thread tooling/generate-css-data.js Resolved
… them

A declaration's value is one component 92 times in 100 (38151 of 41387 on
the Tailwind fixture), and the printer materialized the whole child list
to read its length and first entry. `_valueComponents` and the four
shorthand collapses now walk the child span off the node, so the list is
built only where one is really needed — a custom property's text, a
`unicode-range`, an attribute selector. Young-generation collections for
one Tailwind minify drop from 173 to 165 under a fixed semi-space, and
the emitted bytes are unchanged.
…d guard

A stale Prettier reformatted eighteen template interpolations the branch
never touched; Prettier 3 leaves interpolations alone, so `fmt:check`
passed either way and the churn stayed in the diff.

The guard that makes a spec change fail generation now takes its tables
the way the other collectors do, so a test can drive it.
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging perf/css-minify-structural into main will be
99.25%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.82%100%100%98.82%103
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.12%100%100%99.12%214, 234
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.44%100%100%98.44%1673, 1992, 1999, 2007, 2029, 2032, 2971, 3450–3451, 3483, 4192, 4222, 4275–4276, 4280, 4285, 4301–4302, 4316–4317, 4322–4323, 4843, 4869, 549, 554, 5679, 5711, 5728, 5746, 5762, 5777, 5802–5803, 5805, 6140, 6145, 6151, 6154, 6161, 6173, 6175, 6179, 6197, 6212, 6246, 6302, 6326, 6442, 801–802
   Compiler.js99.56%100%100%99.56%1168–1169, 1177
   ConcatenationScope.js99.12%100%100%99.12%300
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1080, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%480, 527
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.67%100%100%98.67%1220, 1223, 519–523, 525, 671
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.53%100%100%99.53%186, 2454–2455, 2458, 2469, 2480, 2491, 284, 3928, 3943, 3967
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.36%100%100%98.36%504, 513, 516, 520, 532
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.51%100%100%98.51%1303, 1308, 1368, 1382, 1444, 1453
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.90%100%100%98.90%111, 113
   ModuleGraph.js99.78%100%100%99.78%1170
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.72%100%100%99.72%721
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.99%100%100%97.99%1027, 1030, 1047, 1064, 1312, 1346, 1362, 1817, 2114, 2119–2129, 29
   NormalModuleFactory.js98.77%100%100%98.77%1142, 1410, 1421, 1431, 1482–1484, 1491, 536, 548
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%690
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js95.70%100%100%95.70%319, 375, 384, 387, 411, 429, 450–451, 474, 494–495, 531–532, 555, 568–569, 641, 654, 675, 694
   RuntimeTemplate.js99.90%100%100%99.90%119
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.63%100%100%98.63%220, 224, 226, 420, 431, 890
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.47%100%100%99.47%352–353
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%376
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.73%100%100%99.73%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.12%100%100%97.12%10, 267, 289, 291
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js98.15%100%100%98.15%330, 354, 357, 487, 49, 54
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js92%100%100%92%34, 43
   MemoryWithGcCachePlugin.js93.42%100%100%93.42%108, 122–123, 132, 90
   PackFileCacheStrategy.js96.52%100%100%96.52%1310, 1410, 1414, 1476, 1712, 1796, 1819, 1851, 675, 694, 704–706, 708, 724–725, 730, 733, 735, 740, 745, 770, 776, 810,

@alexander-akait
alexander-akait merged commit 4015bbd into main Aug 11, 2026
64 checks passed
@alexander-akait
alexander-akait deleted the perf/css-minify-structural branch August 11, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: css CSS support (lib/css) area: tooling Repo tooling, codegen, CLI entry (tooling/, bin/)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant