Skip to content

Optimize mergeRule selector merging with WeakMap caching#1748

Merged
ludofischer merged 1 commit intocssnano:masterfrom
Goodwine:master
Mar 4, 2026
Merged

Optimize mergeRule selector merging with WeakMap caching#1748
ludofischer merged 1 commit intocssnano:masterfrom
Goodwine:master

Conversation

@Goodwine
Copy link
Copy Markdown
Contributor

Fixes #1747

In the original bug, I reported that processing the file from my example resulted in 1 minute and 800M to parse 20k rules.

With the performance improvements in this CL, the same code took 7s and 360M

Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.87
Maximum resident set size (kbytes): 360260

The main gains come from not having to reparse selectors every time, the getter is very expensive

@alexander-akait
Copy link
Copy Markdown
Member

Some tests are failed, can you look at them?

@Goodwine
Copy link
Copy Markdown
Contributor Author

Ah, I was pretty sure pnmp test was working, maybe I missed something, I'll check again later today :)

@Goodwine Goodwine force-pushed the master branch 3 times, most recently from 0681f8c to 405e970 Compare February 27, 2026 00:26
@Goodwine
Copy link
Copy Markdown
Contributor Author

ok... it was working, but I did a cleanup and ended up removing a flush that was actually necessary 😅, that was tricky to debug

I also forgot to update the dts files. They should be updated now

@ludofischer
Copy link
Copy Markdown
Collaborator

Could you show how you run the benchmark (so we can check this is really faster)?

@Goodwine
Copy link
Copy Markdown
Contributor Author

Could you show how you run the benchmark (so we can check this is really faster)?

Sure, take this as input:

@for $i from 1 through 20000 {
  .foo-#{$i} {
    color: red;
  }
}

And run the following commands (on my phone currently, I can double check if needed on Monday):

sass input.scss output_large.css

\time -v npx postcss output_large.css > output_min.css

Uses default cssnano config.

I use \ for time so that I can pass -v, otherwise it tries to run the flag as a command 🤣

@Goodwine
Copy link
Copy Markdown
Contributor Author

Goodwine commented Mar 2, 2026

alright, I'm at the PC now. This gist should help https://gist.github.com/Goodwine/3759bf6dd78a180d6153b752a05d00be

  1. Download this gist to a directory next to your cssnano repo
  2. Pull the changes from this PR to your repo.
  3. Run:
npm install
\time -v npm run test

@alexander-akait
Copy link
Copy Markdown
Member

@ludofischer Can you take a look? Thanks

@ludofischer
Copy link
Copy Markdown
Collaborator

It does seem to speed up the test case considerably. I have benchmarked the cli using hyperfine and these are the results:

without the PR:

  Time (mean ± σ):     46.373 s ±  1.104 s    [User: 45.849 s, System: 2.046 s]
  Range (min … max):   45.168 s … 48.938 s    10 runs

WIth the PR applied:

  Time (mean ± σ):      8.717 s ±  0.237 s    [User: 8.028 s, System: 1.806 s]
  Range (min … max):    8.370 s …  9.019 s    10 runs

Copy link
Copy Markdown
Collaborator

@ludofischer ludofischer left a comment

Choose a reason for hiding this comment

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

The performance improvements are real, on the other hand the code becomes a lot more complicated in my view. @alexander-akait Realistically, I don't have much time to dedicate to cssnano lately, so if you're OK with merging I will defer to your choice.


if (!ensureCompatibility(selectors, browsers, compatibilityCache)) {
if (ruleCache.has(ruleA) && ruleCache.has(ruleB)) {
// Both already validated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the cache just used to skip the ensureCompatiblity check?

Copy link
Copy Markdown
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

@ludofischer let's merge and release

@ludofischer ludofischer merged commit da88eca into cssnano:master Mar 4, 2026
13 checks passed
@Goodwine
Copy link
Copy Markdown
Contributor Author

Goodwine commented Mar 4, 2026

Thanks! I understand that this makes the implementation more complex. I've tried to add documentation to explain it without overwhelming the code.

Ultimately, the problem is due to the expensive getters/setters in PostCSS. If PostCSS were to parse lazily, this wouldn't have been an issue (or at least, not as significant 😅).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: mergeRules OOMs with 20k adjacent equal rules

3 participants