Repository navigation
fix(server): make collect linear in the number of rules - #1521
Merged
Merged
Conversation
prettier 3 formats asynchronously, so every prettified snapshot was compared with a pending Promise and 16 of 22 snapshots failed. Nobody noticed because @linaria/server had no test script, so `turbo run test` skipped the package. Use the standalone build of prettier with the postcss plugin, which also works without --experimental-vm-modules, await it, and add the test script the other packages use. One snapshot is re-serialized without escaped quotes by the current jest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
collect moved every top-level rule of the parsed stylesheet into `critical` or `other` with `append`. For a node with a parent, `append` removes it with `splice`, and the moved rule was always at index 0, so one call was O(n²) in the number of top-level rules: about 0.2 s for a 1.6 MB stylesheet on Node 20 and seconds while incremental marking is active. Rules are now collected first. The stylesheet is detached at once with `removeAll`, and only then are the nodes appended. Handled top-level at-rules are no longer spliced out one by one. Two bugs found along the way: - `other` dropped the non-critical rules of an at-rule and repeated its critical rules whenever a non-critical rule came first. The `index - removedNodesFromOther` lookup removed the wrong child. Both copies of the at-rule are now filtered by `isCritical` directly. - `blockedClasses` let every second matching rule into `critical`, because `test` on a regexp with the `g` flag keeps `lastIndex` between selectors. The class regexps no longer use `g`, and the classes from the HTML are deduplicated. Whitespace between rules in the output now follows the source stylesheet. Previously the rules were glued together by the removals. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: e591765 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
postcss 8.4 gives a node appended to a root of two or more nodes the whitespace of the last one, so from the third rule on the output lost the whitespace of the source stylesheet. Append each list of nodes in one call while the root is still empty, and test the raw output. The snapshots of two tests lose blank lines that came from the second rule rather than from the source.
Anber
approved these changes
Oct 26, 2026
Anber
left a comment
Collaborator
There was a problem hiding this comment.
Thanks! There was a small bug, I've fixed it. We'll release this soon.
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.
Motivation
collect()is O(n²) in the number of top-level rules.It moves every top-level rule of the parsed stylesheet into
criticalorotherwithappend. For a node that still has a parent,appendremoves it withsplice. The rule being moved is always at index 0, so every move shifts all remaining nodes.On a 1.6 MB application stylesheet (~13.5k top-level rules), one call takes ~0.2 s on Node 20, and seconds on a busy server while incremental marking is active.
collect()is synchronous, so the event loop is blocked for the whole call on every SSR request. The implementation is the same in 6.3.0 and 8.2.0.While testing, two more bugs showed up:
otherloses the non-critical rules of an at-rule, and repeats its critical ones, whenever a non-critical rule comes first. For example,@media (min-width: 1px) { .n {} .c {} }withclass="c"gives@media (min-width: 1px) { .c {} }inother.blockedClasseslets every second matching rule intocritical. The regexp has thegflag, sotestkeepslastIndexbetween selectors.Summary
removeAll(), and the nodes are appended, so nothing is spliced out of the big root. Handled top-level at-rules are no longer removed one by one either. Keyframes are looked up only where they used to stay: in top-level at-rules that were not handled.handleAtRulefilters both clones of the at-rule withisCriticaldirectly, instead of theindex - removedNodesFromOtherarithmetic.gflag, and the classes from the HTML are deduplicated.@linaria/servertests were not running. The package had notestscript, and since prettier 3 madeformatasync, every prettified snapshot was compared with a pending Promise: 16 of 22 failed onmaster. The first commit awaits the standalone prettier with the postcss plugin, which also works without--experimental-vm-modules, and adds thetestscript.One
collect()call after a warm-up, on N rules.c{i}{color:red}:--stress-incremental-marking, beforeOn a real 1.6 MB stylesheet with the HTML of six pages, a call takes 67–92 ms instead of 164–185 ms on Node 20.
criticalis identical apart from whitespace.Most of the remaining time goes to parsing the stylesheet and building
otheron every call. An API that accepts a prepared stylesheet, or lets callers skipother, could remove that, but it is out of scope here.Test plan
pnpm --filter @linaria/server test: 23 tests, 22 snapshots.New tests cover:
criticalandother;blockedClasses;The first two fail on
master.The snapshot changes are whitespace only: blank lines between rules, as in the source. One more snapshot is re-serialized without escaped quotes.
pnpm --filter @linaria/server typecheck,eslint packages/serverand prettier are clean.Changeset:
@linaria/serverpatch.🤖 Generated with Claude Code