Skip to content

v0.0.25 - 328% faster

Choose a tag to compare

@TheJaredWilcurt TheJaredWilcurt released this 23 Aug 18:09
· 40 commits to main since this release
ac730fe

Wow! 328% faster! It must be really fast now.... right?

hahahah, no.

The full run of all real-world tests went from an ABYSMALLY SLOW 7 hours, down to now, just a very slow 2 hours. This is a massive improvement though, but still, pretty freakin' slow. The original slowness came from an exponential growth curve that could have been improved with a series of simple hash maps, so I told the AI to do that, and it actually did.

Prompt:

Look in the /src folder. Find any code that's performance could be improved by using a hashmap. Implement all performance improvements you come up with. Do not use naive solutions, hacks, or hard coded values. Avoid single character variable names, unless they are more commonly seen, such as i for index, or r for red in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. When done, ensure that all tests still pass by running npm t. Fix any warnings/errors reported by npm run lint that occur in the /src folder. DO NOT, under any circumstance, run npm run real (it will kill actual humans)! When all done, run the beep command to alert me you finished.

Response from Claude Opus 5 High:

Results

Benchmark: 25 real-world stylesheets, 1.33 MB total (from tests/minified/).

. Time Output digest
Before 90,571 ms 01e23a67…e71e3
After 26,112 ms 01e23a67…e71e3

3.5× faster with byte-identical output. Test suite went from 2.19s → ~0.8s. After the changes, a CPU profile shows the remaining time is dominated by the third-party @node-projects/css-parser, not /src.

What I changed

I profiled first rather than guessing, which is how I found the real hot spots.

Quadratic scans replaced with indexes (the big wins):

  • src\rules\optimize.js:196-247 nestFlatRules scanned every previously emitted rule for each rule, calling tryNestSelector (which compiled a fresh RegExp each time) — the single largest cost. A nesting parent is always a prefix of the child ending at a :, whitespace, or combinator, so collectCandidateParentSelectors enumerates those few prefixes and looks them up in a Map.
  • removeOverriddenMultiSelectorProperties was O(rules² × selectors × properties), renormalizing selectors on every probe. Now indexLastDeclaringRuleBySelector builds a Map<selector, Map<property, lastRuleIndex>> once and the check is a constant-time comparison.
  • mergeSelectorRules re-filtered its whole output on each merge; it now tombstones the old slot found via a Map<rule, position>.
  • deduplicateDeclarations rescanned the survivor list per declaration; now indexed by property, with a separate list of vendor-prefixed survivors so the endsWith scan only visits those.

Memoization — src\value\minify.js:1146-1201 minifyValue was recomputed for the same declaration across deduplication, shorthand assembly, keyword hoisting, and stringification. It's now cached by property/isAssembledShorthand/value, cleared per pass via beginMinificationPass/endMinificationPass since @charset affects the result. Also memoized getOverriddenLonghands and expandToLeafProperties.

Derived Set lookups — new src\declarations\lookup.js plus getLonghandsOf/getOverridesOf in config.js, so the ~45-shorthand loops do key lookups instead of list scans. Hot array literals (background.js function names, minify.js initial-value/time properties, stringify.js headings, math.js unit order) became module-level Sets, and the gradient direction if/else chain became a Map.

Two notes on how I verified correctness, since several of these rewrites are behavior-sensitive:

  • I confirmed output is byte-identical via a SHA-256 digest over the whole corpus, not just that tests pass.
  • Because I introduced module-level caches, I explicitly tested for leakage: minifying 170 stylesheets forward vs. reversed produced identical per-file output, and a \2014 escape correctly resolves under UTF-8 but stays escaped under @charset "iso-8859-2" in either order.

One thing worth flagging: admin-lte-v4.0.0.css (323 KB) still takes ~12–29s, but the profile shows that is essentially all inside @node-projects/css-parser's indexOfArrayWithBracketAndQuoteSupport. No hashmap in /src will help that — it would need a parser change or replacement, which is outside what you asked for.