Repository navigation
Closed
Description
What
For a route combining a build-time handler with the shell option —
path("/docs/:slug", Prerender(getParams, handler), { name: "docs", ppr: { ttl, swr } })— produce the complete PPR shell entry at vite build, so the route serves x-rango-shell: HIT from the very first request. Today the composition (verified end-to-end in #697) is: build-time Flight payload + runtime-captured HTML prelude — the first visitor triggers a background capture that replays the build-time segments (no handler execution) and freezes the prelude into the shell store. Producer B removes that last runtime step.
Why
- First-request HIT. No cold MISS, no capture latency, no per-deploy warm-up traffic. A static marketing page is a build-time entry with a long TTL whose live holes (loaders) still stream per request.
- Determinism. The runtime capture path is physics-sensitive (quiet windows, cold module graphs, background-task I/O semantics) — the entire CI hardening campaign in fix(vite): prerender collection ran trieless — catch-all shadowed nested routes; Prerender + ppr composition coverage #697 (capture serialization, queue caps, store-fetch timeouts) exists because of it. Build-time production has none of those variables: no ambient identity (the identity guard is trivially satisfied), no quiet-window race, no waitUntil.
- One model.
docs/design/shell-fast-path.mdpins this as the designed end state: pre-rendering is caching at build time becomes literal — one entry format, two producers (runtime capture / build), one consumer (the shell serve path). The worker cannot tell where an entry came from.
Mechanism sketch
The entry format already carries everything (ShellCacheEntry: prelude, postponed, reactVersion, buildVersion, initialTheme, snapshot incl. the implicit doc segment record from #695/#697, handlerLiveHoles):
- During
expandPrerenderRoutes(build), forppr-flagged prerender routes, additionally run the capture pipeline underBuildContext: Flight-render the resolved segments (masked loaders → holes exactly as at runtime), fizz prerender + abort, collect prelude + postponed. - Persist the entry with the build artifacts; at boot (or first
getShell), seed the shell store from the build manifest — or serve build entries through a read-through layer the same way the prerender payload manifest works today (no static file serving; the worker handles every request, per the pre-rendering hard rule). - TTL semantics: build entries default to no-expiry-until-next-deploy (buildVersion gate already evicts on deploy);
ppr.ttl+ runtime recapture can still refresh on top (SWR recapture becomes the upgrade path from build entry to fresher runtime entry, not the bootstrap path). - Eligibility:
handlerLiveHolesdetection runs identically during the build capture (handler-invoked loaders / nested-thenable handle pushes decline the fast path, same as runtime).
Acceptance
Prerender + pprroute: first-ever request after deploy isx-rango-shell: HITwith the frozen prelude + streaming loader holes (dev + production e2e, test-app + cloudflare-basic).- Loaders stay live per request;
updateTag/buildVersion evict build entries like runtime ones. - No static .html/.rsc served from assets (worker-handles-every-request rule upheld).
- The runtime capture path remains for non-prerendered ppr routes, unchanged.
References
docs/design/shell-fast-path.md— "producer B" section (the unification design).- feat(ppr): shell fast path — HIT tails replay the handler layer; only loaders are dynamic #695 (shell fast path / implicit doc cache), fix(vite): prerender collection ran trieless — catch-all shadowed nested routes; Prerender + ppr composition coverage #697 (Prerender+ppr composition coverage, collection trie fix, capture hardening).
packages/rangojs-router/docs/prerender-api-design.md— build/runtime store architecture this extends.
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat(ppr): producer B — emit the shell entry at build time for preren…
docs(router): on-demand prerender design honors PPR producer B — over…