Skip to content

fix: scope dev defer registrations per HTML render to bound registry growth#160

Closed
uhyo wants to merge 1 commit into
masterfrom
claude/sketch-registration-tracking-267amj
Closed

fix: scope dev defer registrations per HTML render to bound registry growth#160
uhyo wants to merge 1 commit into
masterfrom
claude/sketch-registration-tracking-267amj

Conversation

@uhyo

@uhyo uhyo commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Problem

In dev, deferRegistry is a module-level singleton in the RSC environment. Every HTML render registered fresh defer() entries under random UUIDs and nothing ever evicted them, so memory grew unboundedly over a long dev session (many page loads / HMR re-renders).

Closes #144

Approach

Implements the "track registrations per HTML render" strategy suggested in the issue, extended to a rendered-by tree so that invalidating a top-level render invalidates all of its nested defer() registrations too:

  • DeferRegistry#startRender(key, fn): the dev server wraps each top-level HTML render and each HMR RSC re-fetch in an AsyncLocalStorage context keyed by entry path. Every defer() registration made during that render is attributed to a RenderHandle for it.
  • Rendered-by tree: when a deferred entry is loaded later (during SSR or a client fetch), its element is rendered inside the owning render's context, so nested defer() calls attach to the same top-level render.
  • Invalidation with grace period: when a new render for the same entry path begins, the previous render's whole tree is evicted after a 30s grace period, so in-flight client fetches for the previous page load still resolve. Late registrations into an already-superseded render re-arm eviction so stragglers are dropped too.
  • Build unaffected: build-time rendering never calls startRender, so unattributed entries are kept until the process exits, as before.

DeferRegistry is extracted from defer.tsx into deferRegistry.ts (with re-exports preserving existing imports) so the lifecycle is unit-testable without the virtual:funstack/config / #rsc-client modules.

Verification

  • Typecheck, lint, and all unit tests pass, including 9 new registry lifecycle tests (eviction, grace period, per-key isolation, nested-tree invalidation, build-mode exemption).
  • All dev-server (28) and production (31) e2e tests pass.
  • Live check against the fixture-ssr-defer dev server: after re-rendering the page, the previous generation's parent and nested payloads kept serving 200 during the grace period, then both returned 404 after 30s, while the current generation stayed at 200 — confirming AsyncLocalStorage attribution holds through real React flight rendering.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sAWXBbDQBMWmdZpgkssXH


Generated by Claude Code

…growth

In dev, deferRegistry is a module-level singleton and every HTML render
registered fresh defer() entries (with random UUIDs) that were never
evicted, so memory grew unboundedly over a dev session (#144).

Track registrations per top-level HTML render: the dev server wraps each
HTML render and HMR RSC re-fetch in DeferRegistry#startRender, which uses
AsyncLocalStorage so that every defer() call made during that render —
including nested defer() calls made when a deferred element is itself
rendered later — is attributed to the same render, forming a rendered-by
tree rooted at the top-level render.

When a new render for the same entry path begins, the previous render's
whole tree is invalidated and evicted after a 30s grace period, so
in-flight client fetches for the previous page load still resolve.
Build-time rendering never scopes renders, so build output is unaffected.

Closes #144

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sAWXBbDQBMWmdZpgkssXH
@uhyo uhyo closed this Jul 21, 2026
@uhyo
uhyo deleted the claude/sketch-registration-tracking-267amj branch July 21, 2026 12:00
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.

Dev server: deferRegistry grows unboundedly across requests

2 participants