Skip to content

feat(cloudflare): prewarm canonical ISR RSC requests - #3002

Open
james-elicx wants to merge 69 commits into
mainfrom
codex/rsc-prewarm-isr
Open

feat(cloudflare): prewarm canonical ISR RSC requests#3002
james-elicx wants to merge 69 commits into
mainfrom
codex/rsc-prewarm-isr

Conversation

@james-elicx

@james-elicx james-elicx commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

Prewarm browser-identical App Router RSC requests for build-discovered routes, without locally rendering every route and without depending on vinext({ prerender }) or --prerender-all.

Core vinext exposes a cache-adapter capability, responseVary: "verbatim". Only adapters that opt into that contract enable canonical RSC sharing; Cloudflare deployment, Worker version targeting, and CDN validation stay in @vinext/cloudflare.

Request identities

An ordinary full RSC request has one definitive identity:

GET /path?_rsc
Accept: text/x-component
RSC: 1

A deterministic loading.tsx shell has its own stable URL key:

GET /path?_rsc=9qLBDIU2NgN178cB
Accept: text/x-component
RSC: 1
Next-Router-Prefetch: 1
Next-Router-Segment-Prefetch: 1
X-Vinext-Rsc-Render-Mode: prefetch-loading-shell

The browser and deploy warmer derive these URLs through the same request-identity code. Separating the loading shell from the full response avoids depending on two Vary representations beneath one public Workers Cache URL; the path plus stable digest is the cache key.

Canonical requests omit Next-Router-State-Tree, Next-Url, source-route fingerprints, interception context, and mounted-slot context. Requests that actually need that context remain hashed and keep their headers: interception/parallel-route payloads, route-tree or individual-segment prefetches, search-dependent shells, rewrites, mounted slots, and other contextual forms are not collapsed into the shared MVP entry.

Build and deploy behavior

The build emits a small server-side warm plan from route discovery. Static routes are known directly; dynamic App and Pages paths query the built runtime for generateStaticParams() or getStaticPaths() when needed, including MDX routes. This does not call runPrerender() and does not render page responses locally.

At deploy time the Cloudflare package:

  1. uploads the new Worker version;
  2. stages it at 0% beside the current 100% version;
  3. applies the real Worker triggers;
  4. requests the production hostname with Cloudflare-Workers-Version-Overrides selecting the uploaded version;
  5. warms discovered HTML, full RSC, and loading-shell keys;
  6. waits 15 seconds for cache propagation;
  7. promotes only after strict warmup succeeds.

The initial queue sends each request once. Only failures are retried after the rest of the queue completes; there is no queue-wide deadline. A response is accepted only when its status, cache policy, CF-Cache-Status, content type, exact supported Vary, and opaque RSC build ID are coherent. Cacheable responses are warmed; coherent no-store plus BYPASS responses are skipped; same-build cacheable redirects and not-found responses are warmed; wrong builds, 5xx responses, missing CDN admission, and contradictory policies fail strict warmup.

Preview aliases continue through the existing example-preview flow; there is no separate preview warmup path. TPR and TOML parsing are unchanged.

Verification

Focused local verification includes:

  • request-identity tests proving client and warmer produce bare ?_rsc for full RSC and the stable digest for loading shells;
  • Cloudflare warm/deploy tests for version overrides, failure-only retries, strict response validation, and the 15-second pre-promotion propagation window;
  • client navigation/cache tests for automatic Link prefetch, explicit full prefetch reuse, and soft navigation;
  • vp check for every touched implementation/test file;
  • vp run vinext#build and vp run @vinext/cloudflare#build.

The deployed E2E uses a deliberately stale seed Worker, stages and warms the new version, promotes it, and polls a no-cache version endpoint using unique URLs. It then requires single-shot pre-existing CDN HIT assertions for both canonical keys before browser navigation can touch them. Browser coverage verifies:

  • automatic Link loading-shell prefetch from two different source routes;
  • full RSC requests on Link navigation;
  • prefetch={true} cache reuse with zero additional RSC request on click;
  • direct Link soft navigation;
  • dynamic RSC remains no-store / BYPASS.

Cloudflare promotion can briefly route an individual later request back to the identifiable seed Worker even after a sustained readiness window. The E2E retries only a browser scenario that receives X-Vinext-Seed-Worker: 1, using a fresh context. A response from the current Worker that is MISS, BYPASS, malformed, or uses the wrong URL/header shape still fails immediately; canonical cache keys are never primed by the test before their one-shot HIT assertions.

Intentional MVP deferrals

  • query-specific route identities;
  • rewritten aliases;
  • route-tree and individual-segment payloads;
  • interception/modal and mounted-slot payloads;
  • search-dependent loading shells and other contextual partial responses.

Those requests retain their existing contextual headers and hashed _rsc identity rather than risking cross-route cache reuse.

@pkg-pr-new

pkg-pr-new Bot commented Aug 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@3002
npm i https://pkg.pr.new/create-vinext-app@3002
npm i https://pkg.pr.new/@vinext/types@3002
npm i https://pkg.pr.new/vinext@3002

commit: d5663fb

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 2a86cec against base 20fdac4 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 141.4 KB 141.8 KB ⚫ +0.3%
Client entry size (gzip) vinext 128.8 KB 129.2 KB ⚫ +0.3%
Dev server cold start vinext 2.87 s 2.87 s ⚫ +0.0%
Production build time vinext 3.03 s 3.03 s ⚫ -0.0%
RSC entry closure size (gzip) vinext 115.6 KB 116.0 KB ⚫ +0.4%
Server bundle size (gzip) vinext 196.8 KB 197.5 KB ⚫ +0.3%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx force-pushed the codex/rsc-prewarm-isr branch from 2a86cec to d5663fb Compare August 21, 2026 22:29
@james-elicx
james-elicx marked this pull request as ready for review August 21, 2026 23:22
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.

1 participant