Skip to content

Dev-time prerender: make it partial — HMR-generation-keyed caching instead of full re-import + full re-render per request #654

Closed

Description

@ivogt

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:

  1. Re-imports the entire entry chain (rscEnv.runner.import(resolvedEntryPath) in src/vite/router-discovery.ts ~897) on EVERY request — re-running createRouter() 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.
  2. 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.
  3. 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.
  4. The dev store never caches: createDevPrerenderStore (src/prerender/store.ts ~67) fetches per request; the memoizing per-key Map exists 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).

  1. Result cache on the endpoint: memoize matchForPrerender output per (routeName, pathname, variant) keyed by generation; serve cached JSON until the generation bumps. This alone removes the per-request render.
  2. 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).
  3. 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).
  4. 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.ts precedent: 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 matchForPrerender at build; a memoized dev endpoint also speeds up any future dev-mode shell seeding.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions