Skip to content

feat(build): publish static app route handlers as assets#2508

Draft
NathanDrake2406 wants to merge 7 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/static-route-handler-assets-on-2506
Draft

feat(build): publish static app route handlers as assets#2508
NathanDrake2406 wants to merge 7 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/static-route-handler-assets-on-2506

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Field Detail
Goal Let explicit permanent static App Router GET route handlers bypass Worker runtime dispatch on Cloudflare.
Stack Stacked on #2506 and reuses its Cloudflare prerender asset publisher.
Core change Prerender eligible route handlers, mark their artifacts, and publish them as Cloudflare Assets only when the #2506 safety gates allow static asset bypass.
Main boundary Default route handlers remain dynamic/API skips. Only non-dynamic GET handlers with dynamic = "force-static", dynamic = "error", or revalidate = false are considered.
Primary files packages/vinext/src/build/prerender.ts, packages/vinext/src/build/cloudflare-prerender-assets.ts, packages/vinext/src/server/prerender-manifest.ts

Why

Explicit permanent static route handlers should not pay the generated RSC entry, request context, tracked request, route dispatch, and response finalisation cost on every Workers request when their response can be safely emitted at build time. The invariant is the same as #2506: ASSETS-first serving is only correct when request transforms and middleware cannot observe the request.

Area Principle / invariant What this PR changes
Route handler semantics Route handlers are dynamic by default Leaves default route handlers as skipped API routes.
Static eligibility Asset publication cannot revalidate or preserve dynamic effects Only accepts explicit permanent static GET handlers, skips finite ISR, revalidate = 0, force-dynamic, dynamic segments, Set-Cookie, no-store, and non-200 statuses.
Cloudflare asset boundary Middleware/config transforms must run before responses Reuses #2506 publisher gates instead of adding a separate publisher path.
App page cache Route-handler bodies are not page HTML/RSC Marks route-handler prerender entries and excludes them from App page cache seeding and pregenerated page path tables.

What changed

Scenario Before After
Default App route handler Skipped as API Still skipped as API
Explicit static non-dynamic GET route handler Skipped as API Rendered at prerender time and represented as a route-handler artifact
Safe Cloudflare asset publication Only App page HTML/RSC artifacts from #2506 Also publishes route-handler body artifacts through the same publisher
Middleware fixture Would be unsafe to publish as asset Renders the artifact but does not copy it to dist/client because #2506 middleware gate skips publication
Maintainer review path
  1. packages/vinext/src/build/static-route-handler-assets.ts: conservative source-level eligibility and safe asset path mapping.
  2. packages/vinext/src/build/prerender.ts: queueing and rendering route-handler artifacts through the built RSC handler with GET.
  3. packages/vinext/src/build/cloudflare-prerender-assets.ts: shared perf(cloudflare): serve static App Router HTML and RSC from Cloudflare Assets, bypassing the Worker #2506 publisher extended for route-handler artifacts.
  4. packages/vinext/src/server/prerender-manifest.ts: route-handler exclusion from App page consumers.
  5. tests/cloudflare-prerender-assets.test.ts, tests/prerender.test.ts, tests/seed-cache.test.ts: publication, render, safety skip, and cache exclusion coverage.
Validation
  • vp check packages/vinext/src/build/cloudflare-prerender-assets.ts packages/vinext/src/build/static-route-handler-assets.ts packages/vinext/src/build/prerender.ts packages/vinext/src/server/prerender-manifest.ts tests/cloudflare-prerender-assets.test.ts tests/prerender.test.ts tests/seed-cache.test.ts
  • vp test run tests/cloudflare-prerender-assets.test.ts tests/seed-cache.test.ts
  • vp test run tests/prerender.test.ts -t "Cloudflare Workers hybrid build"
  • vp run vinext#build
  • Pre-commit hook also ran staged checks, full check, staged tests, and knip.
Risks and compatibility
Non-goals
  • No default route-handler caching.
  • No dynamic segment support.
  • No route-handler runtime fast path.
  • No finite ISR route-handler asset publication.

References

Reference Why it matters
#2506 Provides the Cloudflare App prerender asset publisher this PR reuses.

NathanDrake2406 and others added 5 commits July 4, 2026 00:00
Fully static App Router prerender outputs currently stay server-owned, so Workers still execute the App Router request lifecycle for cache-hit document and RSC responses. That is unnecessary when Cloudflare Assets can represent the response and no middleware, config transforms, slash redirect, basePath, or i18n routing can observe the request.

Add a conservative build-time publisher that copies eligible static App Router HTML/RSC artifacts into the Cloudflare assets directory and emits matching _headers. Skip ISR, Pages routes, /404 and /500 status routes, existing asset collisions, middleware/proxy projects, and request-transforming config.

Tests cover publication, generated headers, middleware/config opt-outs, ISR/pages/status-route skips, and asset-collision skips.
Cloudflare asset publication for prerendered App Router routes could publish static HTML and RSC files from route metadata alone. That bypassed the runtime cache guard that only serves query-bearing requests when render observation proves searchParams were not observed.

The publisher now requires prerender query-invariance metadata before exposing static App assets. Prerender records the proof using the same render observation check as the runtime cache path, and RSC assets require their own proof bit.
The request-pipeline test hard-codes the Vinext-only internal header set. The new prerender query-invariance side-channel is intentionally internal, so CI failed until the test covered that header as stripped and separate from Next.js INTERNAL_HEADERS.
Cloudflare asset publication could still publish generated HTML when a matching RSC asset already existed and generated RSC publication was disabled by query proof. That left Cloudflare serving mixed route semantics: generated HTML for the document request and an unrelated existing asset for the RSC request.

The route collision guard now treats any existing RSC target as a route-level collision, independent of whether generated RSC will be copied. The regression test pins the html-proof true, rsc-proof false case with an existing about.rsc asset.
@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2508
npm i https://pkg.pr.new/vinext@2508

commit: 9d963df

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 9d963df against base c84f5d4 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 126.4 KB 126.7 KB ⚫ +0.2%
Client entry size (gzip) vinext 120.5 KB 120.7 KB ⚫ +0.2%
Dev server cold start vinext 2.54 s 2.51 s ⚫ -1.4%
Production build time vinext 3.18 s 3.18 s ⚫ +0.0%
RSC entry closure size (gzip) vinext 98.0 KB 98.4 KB ⚫ +0.4%
Server bundle size (gzip) vinext 164.5 KB 164.9 KB ⚫ +0.2%

View detailed results and traces

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

Static App Router HTML assets can bypass the Worker on Cloudflare, but browser RSC requests cannot share the public document URL because Assets matches before the Worker sees RSC headers. Publishing document HTML at the visible route path without a separate RSC transport lets /about?_rsc collide with /about.

Route RSC requests through a Cloudflare-only split transport: plain proven-static RSC uses a reserved static asset namespace, variant RSC uses a Worker-owned namespace that maps back to the visible route before dispatch, and publication stays gated by query proof plus request-transform safety checks.

Tests cover Cloudflare transport URL selection, stale transport redirect canonicalisation, reserved RSC publication, and collision safety.
Explicitly static App Router GET route handlers currently prerender like API skips, so Cloudflare Workers still dispatch through the generated RSC entry and request-state plumbing before running user code.

The missing invariant is that permanent static route-handler responses can use the same Cloudflare asset bypass boundary as static App page artifacts when request transforms and middleware cannot observe the request.

Render conservative non-dynamic GET route handlers during prerender, mark their artifacts in the manifest, publish them through the shared Cloudflare prerender asset publisher, and exclude them from App page cache seeding and pregenerated page path tables.
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/static-route-handler-assets-on-2506 branch from 8d889f6 to 9d963df Compare July 3, 2026 16:02
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