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

Skip to content

Perf: dispatch id/class attribute mutations through the token index (#150 Tier 2)#158

Merged
twschiller merged 1 commit into
mainfrom
perf/dispatch-id-class-attribute-mutations
Jun 5, 2026
Merged

Perf: dispatch id/class attribute mutations through the token index (#150 Tier 2)#158
twschiller merged 1 commit into
mainfrom
perf/dispatch-id-class-attribute-mutations

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Stacks on #157.

Summary

Closes the one behavioral regression flagged in #157's risk section: attribute mutations on already-inserted elements. Before this PR, the token-index dispatcher only walked added subtree roots, so jQuery-style element.id = 'cookie-banner' or classList.add('cookie-banner') after insertion was silently missed. The old code (pre-#157) caught these incidentally because every batch ran a full-doc QSA.

Changes

  • lib/subtree-watcher.ts: opt-in observeAttributes?: boolean on createSubtreeWatcher. When any subscriber asks, the shared MutationObserver upgrades to { childList, subtree, attributes, attributeFilter: ['id', 'class'] }. The router fans attribute mutations only to subscribers that opted in — other rules don't see them.
  • Router dynamically upgrades when an attribute subscriber joins after a non-attribute one and downgrades when the last attribute subscriber leaves, via MO.observe() re-call on the same target.
  • lib/selector-token-index.ts: dispatcher opts in. Attribute targets flow through findTriggeredRules the same way as added subtree roots.

Why opt-in (not blanket)

22 other rules use createSubtreeWatcher directly (pii-redact, secrets-redact, json-ld-sanitize, etc.). Most don't benefit from re-scanning a node just because a class toggled, and pages doing animation-driven class toggling could produce MO storms. The opt-in keeps the existing fast path for those rules intact; only the selector-hide-rule dispatcher pays the attribute-observation cost.

What about MO config replacement semantics

Per the DOM spec, calling observe() on the same MO with the same target updates the registered observer's options in place — no historical record re-emission. We rely on that for both the upgrade and downgrade paths. The router holds observingAttributes to make the desired config recomputable from current subscribers.

Test plan

  • bun run test — 1254 / 1254 pass
  • bun run typecheck
  • bun run check (biome + eslint)
  • bun run knip
  • New subtree-watcher tests cover: attribute mutation delivered as a subtree root, opt-out subscribers ignored, dynamic upgrade after a non-attribute subscriber joined first, downgrade after the last attribute subscriber leaves, placeholder gate still applies, detached-target gate still applies, and the no-op case where no subscriber opted in.
  • selector-hide-rule integration tests cover the canonical regression scenarios: a generic <div> is inserted with no id/class, page later sets div.id = 'late-id' or div.classList.add('late-class'), the rule's removeEntirely path fires on the second drain.

🤖 Generated with Claude Code

@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:26am

Request Review

Base automatically changed from perf/id-class-token-index to main June 5, 2026 11:24
…150 Tier 2)

Stacks on #157. Closes the regression flagged in that PR's risk
section: a jQuery-style `element.id = 'x'` or `classList.add('x')` on
an already-inserted element used to be incidentally caught by the old
full-doc rescan, but the token-index dispatcher's per-added-subtree
walk would miss it.

Adds an opt-in `observeAttributes` flag to createSubtreeWatcher. When
any subscriber asks for it, the shared MO upgrades to
`attributes: true, attributeFilter: ['id', 'class']` — and the router
fans attribute mutations only to subscribers that requested them,
keeping the existing rules off the hot path. The token-index opts in
so its dispatcher re-walks the changed element through the same
findTriggeredRules path as a freshly-added subtree.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@twschiller twschiller force-pushed the perf/dispatch-id-class-attribute-mutations branch from 4ba0697 to 33e072a Compare June 5, 2026 11:26
@twschiller twschiller merged commit 8fa0dad into main Jun 5, 2026
7 checks passed
@twschiller twschiller deleted the perf/dispatch-id-class-attribute-mutations branch June 5, 2026 11:27
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