Repository navigation
perf(css,dom): cache Node.Classes() and drop the per-match class map - #64
Merged
Merged
Conversation
pprof against tailwindcss.com (the heaviest utility-class page in the bench corpus) showed css.compound.matches at 45%+ of total CPU time, with strings.Fields and mapassign_faststr as the top two flat costs. Two compounding causes, both in the single hottest path in the cascade (every candidate rule is checked against every element): 1. dom.Node.Classes() re-split Attr["class"] with strings.Fields on every call, with no caching at all. 2. compound.matches then built a brand-new map[string]bool from that result on every single selector check, just to test membership of a handful of wanted classes. Classes() now memoizes its split, self-invalidating by comparing the raw attribute string on each call (there is no dedicated class-list setter to hook — a script can write Attr["class"] directly, see js/style.go) rather than relying on an explicit invalidation call site. compound.matches now does a plain linear scan instead of building a map: both class lists are small enough that hashing costs more than it saves. Rendering tailwindcss.com dropped from ~5.7s to ~2.2s wall-clock (measured with cmd/render, network fetch included); a follow-up pprof run confirms css.Selector.Matches fell from 45% to ~10% of total CPU time.
tannevaled
added a commit
that referenced
this pull request
Aug 31, 2026
Regenerates results.json/REPORT.md's table and montages against the merged grid track-sizing fix (#63), the esbuild sandbox-escape fix (#65), and the two perf dedupe wins (#64, #66). Records all four in FIDELITY.md's dated log, closes the CSS Grid "Known gaps" bullet (now fixed), and adds an honest note on tailwindcss.com's SSIM number after visually checking the montage.
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.
Summary
dom.Node.Classes()now memoizes its split ofAttr["class"]instead of re-runningstrings.Fieldson every call.css.compound.matchesnow does a linear scan for class membership instead of allocating a freshmap[string]boolon every single selector check.css.compound.matcheswas 45%+ of total CPU time, withstrings.Fieldsandmapassign_faststrthe top two flat costs — both are on the single hottest path in the cascade (every candidate rule checked against every element).Test plan
go test -short ./...— full suite green.go test -cover ./dom/... ./css/...— both packages still meet their coverage-gate floor (dom 97.7%≥97.4%, css 99.5%≥99.5%).cmd/render: tailwindcss.com wall-clock ~5.7s → ~2.2s (network fetch included); pprof confirmscss.Selector.Matchesfell from ~45% to ~10% of total CPU.