Skip to content

perf(css,dom): cache Node.Classes() and drop the per-match class map - #64

Merged
tannevaled merged 1 commit into
mainfrom
perf/cache-node-classes-avoid-map-alloc
Aug 31, 2026
Merged

tannevaled merged 1 commit into
mainfrom
perf/cache-node-classes-avoid-map-alloc

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

  • dom.Node.Classes() now memoizes its split of Attr["class"] instead of re-running strings.Fields on every call.
  • css.compound.matches now does a linear scan for class membership instead of allocating a fresh map[string]bool on every single selector check.
  • Found via pprof against tailwindcss.com: css.compound.matches was 45%+ of total CPU time, with strings.Fields and mapassign_faststr the 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%).
  • Measured with cmd/render: tailwindcss.com wall-clock ~5.7s → ~2.2s (network fetch included); pprof confirms css.Selector.Matches fell from ~45% to ~10% of total CPU.

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
tannevaled merged commit 803fd46 into main Aug 31, 2026
7 checks passed
@tannevaled
tannevaled deleted the perf/cache-node-classes-avoid-map-alloc branch August 31, 2026 17:53
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.
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.

1 participant