Repository navigation
Description
Problem (dev-only, perf/DX)
The dev prerender path re-does everything on every request. For non-Node RSC runtimes (workerd/Cloudflare dev), every request to a Prerender route round-trips to /__rsc_prerender, and the endpoint:
- Re-imports the entire entry chain (
rscEnv.runner.import(resolvedEntryPath)insrc/vite/router-discovery.ts~897) on EVERY request — re-runningcreateRouter()and re-evaluating the entry → router → urls → handlers chain even when no module was invalidated. The comment explains why it exists (stale-router-after-edit), but it is generation-blind. - Re-renders the full segment chain via
matchForPrerender(handler + all layouts + parallels), Flight-serializing everything, per request — no memoization, despite the result being deterministic between edits. - Renders ALL intercept variants unconditionally (
src/router/prerender-match.ts~311: "we pre-render all intercepts unconditionally") even when the request asks for the main variant only. - The dev store never caches:
createDevPrerenderStore(src/prerender/store.ts~67) fetches per request; the memoizing per-keyMapexists only in the production manifest path (~99).
Net effect: dev navigation on Prerender routes (especially Cloudflare dev) pays module re-evaluation + full chain render + serialization on every hit, and heavy getParams/handler work (fs reads, DB queries via buildEnv) repeats per navigation.
Direction: key everything to an HMR invalidation generation
Track a dev "prerender generation": bump it when the module graph invalidates anything in the entry → router → urls → handler chain (Vite hot events / onFileChange hooks the plugin already watches for route regen).
- Result cache on the endpoint: memoize
matchForPrerenderoutput per (routeName, pathname, variant) keyed by generation; serve cached JSON until the generation bumps. This alone removes the per-request render. - Generation-gated re-import: skip the entry re-import when the generation is unchanged since the last import (the current re-import becomes the on-dirty path only).
- Variant laziness folds into 1: main and intercept variants cache independently under the same generation key (or render once, cache both — decide by measuring; the unconditional intercept render may be cheap once it is amortized by the cache).
- Optional client-side (workerd store) cache: short-lived per-key cache in
createDevPrerenderStore, revalidated via a generation token echoed in the endpoint response. Likely unnecessary if 1-2 land (the fetch becomes cheap); measure first.
Correctness bar: an edit to ANY module in a prerendered route's chain must still reflect on the next request (the whole point of the current always-re-import). The generation bump must be conservative — over-invalidation is fine, staleness is not.
Test plan
- Unit: generation bump on chain-module invalidation; cache hit path returns identical payload; edit → bump → fresh render.
- E2e (dev-mode HMR territory — note
route-types-hmr.test.tsprecedent: HMR watcher tests are skipped on CI, run locally): edit a prerender handler file, assert next request serves the new content; repeated requests without edits serve from cache (assert via endpoint debug timing or a hit counter header). - Perf sanity: log before/after timings for a Prerender route navigation in cloudflare-basic dev.
Notes
- Production path untouched — this is purely the dev endpoint + dev store.
- Related but separate: PPR: build-time shell capture (prerender × ppr) #649 (build-time shell capture) will reuse
matchForPrerenderat build; a memoized dev endpoint also speeds up any future dev-mode shell seeding.
Activity
perf(vite): memoize dev prerender renders keyed by router-instance id…
perf(vite): memoize dev prerender renders keyed by router-instance id…