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

Skip to content

Perf: SPA route-change re-sweep + detached-subtree fast-path (#150 Tier 1S)#152

Merged
twschiller merged 1 commit into
mainfrom
perf/tier-1s-spa-route-transitions
Jun 5, 2026
Merged

Perf: SPA route-change re-sweep + detached-subtree fast-path (#150 Tier 1S)#152
twschiller merged 1 commit into
mainfrom
perf/tier-1s-spa-route-transitions

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Tier 1S of #150 — targets SPA route transitions specifically. Without these, a React/Vue route swap that mounts thousands of nodes either waits out a 250ms throttle window before the new content is hidden, or misses URL-gated selectors that newly apply to the route.

Summary

lib/route-change.ts (new) — process-wide route-change emitter:

  • Hooks the Navigation API navigatesuccess (modern Chrome) plus popstate and hashchange as belt-and-suspenders.
  • Dedupes by URL so multiple events for the same navigation fire the listener once.
  • Older browsers without Navigation API miss pushState-only routes — but the watcher's normal MutationObserver still catches up. This signal is a latency optimization, not a correctness requirement.

lib/subtree-watcher.ts — wire route changes + detached fast-path:

  • Subscribe to subscribeRouteChange in start(). On a detected change:
    1. Cancel the pending throttle, discard buffered MutationRecords, clear pending.
    2. Schedule a one-frame-later sweep that calls onSubtrees([document.body]).
    3. The rAF wait lets the framework commit the new route's DOM before we scan.
    4. Consecutive route changes coalesce — only the latest scheduled sweep fires.
    5. Skipped while the tab is hidden or the watcher has been stopped.
  • Detached-subtree fast-path in enqueue(): React reconciliation routinely adds-then-removes nodes within one synchronous block. By the time the MO callback fires, the addition record is still there but the node is already detached. drain() filters by isConnected too — checking at enqueue keeps pending small during noisy route-swap bursts.

What Tier 1S items are landed

From issue #150:

Test plan

  • bun run test → 1177 passed (1164 baseline + 6 route-change.test.ts + 7 new subtree-watcher cases)
  • bun run typecheck
  • bun run check (biome + eslint)
  • bun run knip
  • bun run test:coverage — statements 85.55% / branches 76.81% / functions 88.71% / lines 85.46% (above ratchet)
  • Manual smoke on a real SPA (e.g. a React Router app, GitHub web UI) — confirm placeholders apply on initial route AND after a route swap, no double-application, no perf regression on rapid back/forward

Refs #150.

🤖 Generated with Claude Code

Tier 1S of #150 — targets SPA route transitions specifically. Without
these, a React/Vue route swap that mounts thousands of nodes either
waits out a 250ms throttle window before the new content is hidden, or
misses URL-gated selectors that newly apply to the route.

lib/route-change.ts (new):
- Process-wide route-change emitter. Hooks the Navigation API
  (`navigatesuccess`, modern Chrome), plus `popstate` and `hashchange`
  as belt-and-suspenders. Dedupes by URL so multiple events for one
  route fire the listener once. Older browsers without Navigation API
  miss pushState-only routes, but the watcher catches up on the next
  throttle window — this is a latency optimization, not a correctness
  requirement.

lib/subtree-watcher.ts:
- Subscribe to route changes in start(). On a detected change, cancel
  the pending throttle, discard buffered MutationRecords, clear
  pending, then schedule a one-frame-later sweep that calls
  onSubtrees([document.body]). The rAF wait lets the framework
  commit the new route's DOM before we scan. Consecutive route
  changes coalesce — only the latest scheduled sweep fires.
  Skipped while the tab is hidden or the watcher has been stopped.
- Detached-subtree fast-path in enqueue(). React's reconciliation
  routinely adds-then-removes nodes within one synchronous block; by
  the time the MO callback fires, the addition record is still there
  but the node is already detached. drain() filters by isConnected
  too — checking at enqueue keeps pending small during noisy
  route-swap bursts.

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 3:51am

Request Review

@twschiller twschiller merged commit 35adc41 into main Jun 5, 2026
7 checks passed
@twschiller twschiller deleted the perf/tier-1s-spa-route-transitions branch June 5, 2026 03:52
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