Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Perf: per-rule WeakSet for processed-node skip bypass (#150 Tier 2)#159

Merged
twschiller merged 2 commits into
mainfrom
perf/weakset-processed-cache
Jun 5, 2026
Merged

Perf: per-rule WeakSet for processed-node skip bypass (#150 Tier 2)#159
twschiller merged 2 commits into
mainfrom
perf/weakset-processed-cache

Conversation

@twschiller

@twschiller twschiller commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Implements item 14 from issue #150.

Summary

Each selector-hide-rule's scan loop currently does 5 marker reads per surviving candidate (PLACEHOLDER_CLASS classList check, two closest() walks, two getAttribute calls) before deciding to skip or hide. On infinite-scroll feeds and SPA route sweeps the same candidates keep surfacing batch after batch — the dispatcher's QSA re-finds every element the rule already processed, and the marker reads re-run every time.

This PR adds a per-rule WeakSet<HTMLElement> in the factory closure. When the loop conclusively decides to skip (or after it hides), the element joins the set. The next scan short-circuits at the top of the loop.

Why a cache, not a code change to the markers

Markers stay in the DOM for cross-rule coordination (other rules read HIDDEN_ATTR / REVEALED_ATTR) and for the reveal click handlers (the placeholder click flow looks up REVEALED_ATTR on the restored element). The WeakSet is purely a hot-loop bypass for this rule's own re-scans — same conclusions, fewer DOM reads.

Safety boundary

Membership is added only when the skip reason is the element's own state:

Check Memoize? Why
element.classList.contains(PLACEHOLDER_CLASS) Own state; we don't un-placeholder an element
element.closest('.placeholder') Ancestor-relative — the placeholder above could be revealed (replaced), exposing the element
element.getAttribute(REVEALED_ATTR) === id Own state; REVEALED_ATTR is monotonic (only ever set)
element.closest('[REVEALED_ATTR=id]') Ancestor-relative — element could move out from under the wrapper
element.getAttribute(HIDDEN_ATTR) === id Own state; HIDDEN_ATTR is only written by this rule
Hide branches (placeholder or removeEntirely) We just decided this element is processed

If we memoized the ancestor-relative checks, a candidate that moves out of a revealed wrapper would be silently missed on subsequent scans. The tests pin this down explicitly.

Test plan

  • bun run test — 1261 / 1261 pass
  • bun run typecheck
  • bun run check (biome + eslint)
  • bun run knip
  • Example tests cover: own-state memoization (HIDDEN_ATTR strip after hide → element stays in its hidden state, the rule does NOT re-process it), revealed-for-this-rule short-circuit on re-scan, ancestor-relative non-memoization for both closest('.placeholder') and closest('[REVEALED_ATTR=id]') (move-out scenarios), and a spy-based assertion that a placeholder candidate's getAttribute call count doesn't grow between scans.
  • New property tests (two): for random tree shapes, random target masks, and random pre-existing marker seeds per node (none / PLACEHOLDER_CLASS / HIDDEN_ATTR for this rule or another / REVEALED_ATTR for this rule or another), the DOM after one apply equals the DOM after N applies (N up to 6). One property for placeholder mode, one for removeEntirely. A regression in WeakSet membership — added when it shouldn't be, or not added when it should be — would surface as innerHTML drift between the first apply and the N-th.

🤖 Generated with Claude Code

Adds a WeakSet<HTMLElement> in each selector-hide-rule's closure that
caches "already concluded skip" for elements based on their own state
(PLACEHOLDER_CLASS, REVEALED_ATTR === id, HIDDEN_ATTR === id, and the
hide branches). Subsequent scans short-circuit at the top of the loop,
saving the 5 marker reads on infinite-scroll feeds and SPA route
sweeps where the dispatcher's QSA re-finds every already-processed
candidate.

Markers stay in the DOM for cross-rule coordination and for reveal
click handlers; the WeakSet is a perf bypass only. Ancestor-relative
skips (closest('.placeholder') / closest('[REVEALED_ATTR=id]'))
intentionally do NOT add to the set — the element could later move
out from under the matched ancestor, and a memoized skip would
silently miss the new eligibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 5, 2026 11:43am

Request Review

Strongest property the WeakSet bypass must satisfy: with the DOM held
fixed between calls, apply once produces the same DOM as apply N
(for N in [2, 6]). Fuzzes random tree shape, random target mask, and
random pre-existing marker seed per node (none / PLACEHOLDER_CLASS /
HIDDEN_ATTR for this rule / HIDDEN_ATTR for another rule /
REVEALED_ATTR for this rule / REVEALED_ATTR for another rule). Two
properties: one for placeholder mode and one for removeEntirely mode.

A divergence — innerHTML drifting between the first apply and the
N-th — would surface any case where the WeakSet's membership decision
disagrees with the marker-based skip ladder.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@twschiller twschiller merged commit 980a5b1 into main Jun 5, 2026
7 checks passed
@twschiller twschiller deleted the perf/weakset-processed-cache branch June 5, 2026 11:45
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