perf: stylesheet-aware snapshot (PERF-5) + cache/bloom/bg reuse - #492
perf: stylesheet-aware snapshot (PERF-5) + cache/bloom/bg reuse#492thelabcorner wants to merge 2 commits into
Conversation
Single coherent perf pass on the snapshot hot path — 1.82x diverse / 5.9x huge vs upstream/main (neutral snapdom.toRaw()), 0px diff via window.__SNAPDOM_FULL_PROPS (allow 137 vs 370 props). - css: split getStyle caches (WeakMap) + epoch invalidation via bumpEpoch() -> _invalidateSplitCaches(), frozen emptyStyle (§6/§8) - styles: allow-list scans author sheets + @import + adopted + shadow, shorthands->longhands, inline-seed top-down, UA diff 19 tags, A2 clone-before-mutate, B2 SVG early-return, C3/C8 hoists (§4) - background: reuse snapshot via getCachedSnapshot (§6) - pseudo: bloom filter for ::before/::after/::first-letter, now covers @import + shadow same as allow-list (§8) - tests: 110 passed / 1 failed / 3 skipped (d489 2.82% fails on clean upstream stashed, isolation proof), guard __tests__/utils.css.splitcache Each intermediate was 110/1 green; squash for one big PR. Happy to split into 4 PRs on request. Base: main (26/26 recent merges -> main). Rebased on upstream/main, no scratch/.idea, no whitespace churn.
|
Hi Jackson, thanks a lot for this, and for the care that went into writing it up. The I took it to a separate checkout and tested it in a few scenarios beyond the test suite.
I also measured where the speed comes from, on a repetitive 1950 node DOM, median of 7 runs:
So the win is essentially all from those two mechanisms. The rest of the pass (hoisted The direction is right, and it is the same one v3 takes: v3 already ships stylesheet-aware Genuinely, thank you. I would really like to have you back once v3 is out: the hot path |
F1 cross-origin fallback to full read per doc. F2 adaptive UA probe reusing snapshot declaration, no extra gcs. F3 structural sharing removed. Round-2: shorthand-free allow set, capture-scoped shadow scan, container-rule fingerprint dedup, author-vars gate for CSSVar, flex-parent memo, cached getStyle for IMG and placeholder. Guards: juan-regressions (4), residual (8), CSSVar skips (2), all mutation-tested. Suite: 896 passed / 1 failed (d489 environmental, also fails on upstream/main) / 3 skipped.
|
Hi Juan, Where the branch is now: Cross-origin stylesheets. If any sheet throws on cssRules, that document falls back to the full computed-style read, cached per epoch. There's a regression test that mocks a denying sheet and checks letter-spacing, text-transform, font-style and text-indent come out identical to the full-read control. UA diff. The missing import is just my mistake. (Gotta love agents! Haha.) I had the agent running vitest directly and never hit the lint gate, so I never saw the no-undef, which also means the safeguard I described in the PR body was dead code the whole time. (lol) -- Fixed, and I replaced the sandbox probe with one that reuses the computed declaration the snapshot already holds, so it costs no extra getComputedStyle calls. Guarded for pre, th, em and ol with no author CSS. Removed the structural cache. Your nth-child case and the two .card divs are both right, and I don't think that key design can be rescued, so I just got rid of it aswell. Your breakdown matches what I see, and thanks for publishing it! Knowing the micro-opts land at roughly zero on that shape is useful on its own. 😊 On landing it, point taken, (and I somewhat anticipated it! haha!) One note on the latest commit: I just pushed my current work to the PR branch just so it is there, even if you close the PR. I have been working on this for a little bit in my codebase & decided to share it, so others doing perf passes can see what I found or reuse the code if they want to. I will personally keep running this v2-based branch in my own codebase until v3 is stable and I have had time to update and test everything against it! Context on why I went at this: I'm building PresGEN, a browser-first presentation tool, and I have a LOT of capture-heavy paths using snapDOM, so snapshot time pays me back directly! (I'll also admit I just like this kind of work! Instrumenting a hot path and watching a counter fall, can't beat the feeling!!!) Once v3 settles, I'd be more than glad to do a perf pass on top of it on my own time! Disclosure: as noted on the initial PR, I work with AI coding agents. The missing import getting past me was just me not watching my agent closely enough, so I have earned the lint gate lesson. This time I had separate agents verify each claim in this reply against the diff and the test output before I wrote it up. The fixes above are pushed as the latest commit on this PR branch, so they are there if you want to look, and fine to ignore if you would rather close it. Branch state, for reference: Full suite: 896 passed, 1 failed, 3 skipped. The failure (d489-reconcile-transform) still fails here. Style-read counts on a ~900 node bench DOM through the public pipeline, same DOM both sides: getPropertyValue 384,449 to 153,641, getComputedStyle 3,969 to 2,366. Thanks again, I've gotten more use out of this project than I can put into words! |
perf: stylesheet-aware snapshot + cache/bloom reuse
Hi Juan, thanks for Snapdom and for all the work on v3! Wanted to share a perf pass I’ve been testing on
2.24.xin case it’s useful. I saw v3 is adding stylesheet-aware scanning and auto memoization, so happy to retarget this there if you prefer.Why this PR
snapdom.toRaw()spends most of its snapshot phase reading~370computed props per node withgetPropertyValue. Most of those are just browser defaults. On same-processsnapdom.toRaw()vsupstream/main(2dc5348):3563msto599ms(5.9x)332msto182ms(1.82x,BENCH-DIVERSE2.04x)window.__SNAPDOM_FULL_PROPS=1vs allow-list: 0px diffWhat changed
All on one hot path (
src/utils/css.jsandsrc/modules/styles.js/background.js/pseudo.js):document.styleSheetsplus@import,adoptedStyleSheetsand shadow roots once per epoch, expands shorthands to longhands and seeds with inline styles top-down.snapshotComputedStyleFullthen iterates 137 props instead of 370.MODULE_REQUIRED_PROPSplus a UA diff for 19 tags keepspre,th,tableand others correct.window.__SNAPDOM_FULL_PROPSforces a full read for verification.getStylenow uses splitWeakMapcaches fornulland pseudo, bridged tocache.computedStyleand cleared onbumpEpochso it does not return stale values after a mutation.emptyStyleis frozen.background.jsreuses the snapshot withgetCachedSnapshotinstead of 24k extra reads.pseudo.jscollects selectors for::before,::afterand::first-letterincluding@importand shadow, and skipsgetComputedStylewhenmatchessays there is nothing to do.No new
options, noscratchor.idea, based onmainand rebased on currentupstream/main. The whitespace diff that shows up is just the functionalif (!allow)guard (52 lines).Tests
npx vitest run --browser.headlessgives 110 passed, 1 failed, 3 skipped. The one failure isd489-reconcile-transformat 2.82 percent, and it also fails on a cleanupstream/mainstash. Added a small guard test__tests__/utils.css.splitcache.test.jsfor the epoch fix.Note on AI
I used AI (Muse Spark via OpenCode) to help iterate, measure and draft. All benchmarks are same-process medians with ranges checked, and the 0px check is in the repo so you can rerun it.
Really appreciate your work on this project. Happy to split this into separate PRs if you would like, just let me know. Thanks!