Skip to content

fix: cache fs-routes enumeration across dev requests - #180

Merged
uhyo merged 2 commits into
masterfrom
claude/fs-routes-chunk-eviction-irzftf
Aug 30, 2026
Merged

fix: cache fs-routes enumeration across dev requests#180
uhyo merged 2 commits into
masterfrom
claude/fs-routes-chunk-eviction-irzftf

Conversation

@uhyo

@uhyo uhyo commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Problem

Every dev-server request iterates getEntries(), and the fs-routes implementation did all of its work per invocation. So on every request (HTML and HMR RSC re-fetches alike):

  • the route tree was rebuilt and every dynamic route's generateStaticParams() re-ran — user code that often fetches data, making large sites pay the full-site enumeration cost per request;
  • one chunk per Server Component node × params combination was re-registered in the dev defer registry under a fresh random ID.

The registration flood interacts badly with the registry's eviction policy (maxPending: 1000, oldest first): a site with C chunks evicts a tab's chunks after ~1000/C further requests. Payloads already served bake the old chunk IDs into their slots, so when a long-idle tab soft-navigates, the chunk fetch 404s and FsRouteChunkBoundary falls back to a hard navigation.

Fix

createFsRoutesEntriesWithHost now enumerates the site once per entries-module instance and caches the result (route tree, pages, node metas, registered chunks); getEntries() iterations reuse it:

  • Dev requests no longer re-run generateStaticParams() or re-register chunks; chunk IDs stay stable across requests, so eviction pressure from fs-routes disappears entirely.
  • Chunks the dev registry still evicts (the 5-minute TTL on settled entries, or maxPending pressure from defer() calls) are re-registered under their original IDs on the next request, via two new host hooks (hasChunk/restoreChunk), so pages held by open tabs stay soft-navigable.
  • HMR keeps working: editing a routed file invalidates the entries module (the virtual module embeds the import.meta.glob), so a fresh module instance re-enumerates.
  • Builds are unaffected — they iterate getEntries() once.
  • A failed enumeration is not cached, so a transient generateStaticParams() error (e.g. a CMS hiccup) doesn't stick for the rest of the dev session.

Behavior note: generateStaticParams() results derived from external data are now cached in dev until a routed file is edited or the server restarts. Documented in the file-system routing docs.

Testing

  • Unit tests added: enumeration runs once across iterations with stable chunk IDs, evicted chunks are restored under their original IDs, and a failed enumeration is retried.
  • pnpm typecheck, pnpm lint, pnpm format:check, pnpm test:run all pass.
  • Full e2e suites pass: 39/39 (build/preview) and 32/32 (dev server), including all fs-routing dev tests.

This addresses the dev-request cost of the existing enumeration model; a follow-up issue covers the larger improvement of not enumerating the whole site to serve one request (also for the non-fs entries.tsx mode).

🤖 Generated with Claude Code

https://claude.ai/code/session_016D8ULaGdQjQ493LBy3ow1S


Generated by Claude Code

Every dev request iterated getEntries(), re-running the whole site
enumeration — including every generateStaticParams() — and registering a
fresh set of chunks under new random IDs. On large sites this made every
request pay the full-site cost, and the flood of registrations pushed
older chunk IDs out of the dev defer registry, so long-idle tabs lost
their soft-navigation chunks and fell back to hard navigation.

The enumeration (route tree, generateStaticParams(), chunk registration)
now runs once per entries-module instance and is cached; editing a routed
file invalidates the module in dev and re-enumerates, and a build iterates
getEntries() only once so the cache is inert there. Chunk IDs stay stable
across requests, and any chunk the dev registry evicts is re-registered
under its original ID on the next request, keeping payloads held by open
tabs soft-navigable. A failed enumeration is not cached, so a transient
generateStaticParams() error does not stick for the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016D8ULaGdQjQ493LBy3ow1S
…stry

The restore pass runs only when getEntries() is iterated (HTML requests
and HMR re-fetches), but a chunk fetch on soft navigation goes through
serveRSC's module path, which never iterates the entries. With chunk IDs
now stable across requests, a settled chunk idle past the eviction TTL
could be dropped — by evictStale at the start of the very request
fetching it — and 404, falling back to hard navigation. On a registry
miss, iterate the entries once (cheap: the enumeration is cached) to
re-register the current chunk set under its original IDs, then retry the
lookup before returning 404.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016D8ULaGdQjQ493LBy3ow1S
@uhyo
uhyo merged commit 834258d into master Aug 30, 2026
2 checks passed
@uhyo
uhyo deleted the claude/fs-routes-chunk-eviction-irzftf branch August 30, 2026 10:37
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.

2 participants