Repository navigation
perf(vite): dev prerender result cache keyed by router-instance identity, CF handler-edit freshness fix - #662
Conversation
…entity (#654) The /__rsc_prerender endpoint re-rendered the full segment chain (and re-ran getParams) on every request. Results are now cached per router instance: the per-request entry re-import re-runs createRouter() exactly when the HMR chain was invalidated, so instance identity IS the edit generation (WeakMap; stale generations GC). One render warms both the main and intercept variant keys; x-rango-prerender-cache: HIT|MISS reports the outcome. The Cloudflare temp-runner path now re-imports the entry per request too, fixing handler-only-edit staleness (files without urls()/createRouter() never triggered a temp-registry refresh, so edits served stale prerender content until a route-file edit).
d8e9c0d to
ecd8def
Compare
|
Hardened the HIT assertions against concurrent-invalidation flakes (review feedback on test staging): Interference surface mapped first. Every file-writing suite in both apps lives in a dedicated Assertions now immune by construction. The MISS assertions were already churn-proof (first request for a key has no cached body under ANY identity). The HIT assertions are now paired polls — each attempt issues the request pair and requires the second response to be a HIT byte-identical to the first; a legitimate identity churn between the pair retries, while a broken cache never returns HIT and times out. The variant-warming attempt re-warms via the main variant each iteration; its precise both-keys-from-one-render semantics stay pinned by the unit tests. Adversarial verification: ran the dev suite while a loop toggled |
…r-cache # Conflicts: # packages/rangojs-router/docs/internal/feature-file-map.md
commit: |
…, dev prerender memoization (#662); map loader-snapshot.ts
Closes #654.
Problem
In dev, every request to a prerendered route re-did all the work: the
/__rsc_prerenderendpoint re-imported the entry chain and re-rendered the full segment chain (handler + layouts + parallels + all intercept variants), re-runninggetParams()/handler side effects (fs reads, DB via buildEnv) per navigation — despite the result being deterministic between edits. On the Cloudflare path there was also a correctness bug hiding under the perf problem: the temp Node runner's registry was only refreshed on route-file edits (files matching theurls(/createRouter(sniff), so a handler-only edit served stale prerender content indefinitely.Before vs. after (consumer's seat)
Consumers configure nothing. Dev now matches production semantics more closely: prerender output is frozen between edits (production freezes it at build), instead of re-rendering per request.
How it works (the invariant)
The endpoint re-imports the user's entry through a module runner on every request — the Node preset on the main RSC environment (pre-existing, commit
11434658), the Cloudflare preset now on the shared temp Node server (new). When any module in the entry → router → urls → handler chain was HMR-invalidated, that import re-runscreateRouter()andRouterRegistry.setreplaces the router instance with a new object; when nothing changed, the import is a module-cache hit and the instance is the same object. Router-instance identity therefore IS the HMR generation, and the cache is aWeakMap<routerInstance, Map<key, body>>(src/vite/discovery/dev-prerender-cache.ts) — no watcher bookkeeping, no invalidation calls, stale generations garbage-collect when the registry drops the old instance. One render warms both the main andintercept=1variant keys (matchForPrerender computes intercept segments unconditionally). Cache key = pathname × intercept × passthrough × routeName. Errors, passthroughs, and non-matches are never cached; multi-router fall-through checks the cache per router in registry loop order, preserving uncached semantics exactly.Semantics table
HIT— Map lookupHIT(warmed by the main render)Code changes
src/vite/discovery/dev-prerender-cache.ts(new, ~120 lines): identity-keyed cache + key derivation + variant payload derivation; FIFO cap 500/router. Chosen as a pure module invite/discovery/(the plugin-discovery helpers' home per feature-file-map) so the semantics are unit-testable without a Vite server.src/vite/router-discovery.ts: endpoint consults the cache per router beforematchForPrerender, warms both variant keys on a render, setsx-rango-prerender-cache; the Cloudflare temp path gains a per-requestimportEntryAndRegistry(mirrors the Node path's freshness mechanism; warm imports are module-cache hits).prerender-api-design.mddev-mode section rewritten (also fixes a stale claim — the Node preset DOES set__PRERENDER_DEV_URLand round-trips the endpoint; the doc said otherwise), feature-map + feature-file-map rows.Measured (local, M-series, trivial fixture handlers)
/docs/getting-started)/articles/page/1)Absolute deltas are small on fixture apps because their handlers are trivial; the structural win is that HITs skip
matchForPrerender+getParamsentirely, so it scales with real handler cost (fs/DB work — the issue's motivation). Cloudflare handler-edit freshness verified live: editreleases-handler.tsx→ fresh content in ~2s → next request HIT (this exact loop served stale content on the old code).Test plan
src/vite/discovery/__tests__/dev-prerender-cache.test.ts): identity semantics (new instance = new generation), variant-payload parity with the endpoint's historical format (intercept merge, no-intercept fallback, missing-handles fallback), FIFO cap behavior, key dimensions.prerender-dev-cache.test.ts— MISS→HIT byte-identical body; intercept-variant warmed by one render;(production)counterpart pins no header leak + endpoint absent in prod. Red-before-green: run against the pre-changedistfails at the header assertion (Expected: "MISS", Received: undefined); green after rebuild — 9/9 across--repeat-each=3with retries disabled.prerender-dev.test.ts; production header-absence case inprerender.test.ts. 3/3 dev, 1/1 production.prerender-hmrtests pass unchanged — including the imported-component regression case the per-request re-import exists for — plus a new endpoint-level case: warm HIT → edit → MISS with fresh content → converges back to HIT. 4/4.route-types-hmrrun locally per the Vite-plugin-change mandate (17 passed, 1 flaky-recovered — watcher timing, the reason the suite is CI-skipped).pnpm run typecheck,pnpm run test:unit:all(router 4815 + demo 26 + mini 23 + cloudflare-basic 46 + RSC suites),pnpm run lint,pnpm run format,check:e2e-bucketing,check:doc-links— all green.Notes
getParamsenumeration follows the same rule.createDevPrerenderStore(item 4) — measured HITs are ~22ms round-trip, so a store-side cache would save little; revisit only with evidence.matchForPrerenderat build; this memoized endpoint also speeds up any future dev-mode shell seeding, per the issue's note.