Skip to content

perf(cloudflare): publish unaffected static app routes with config transforms#2510

Draft
NathanDrake2406 wants to merge 8 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/static-asset-route-eligibility
Draft

perf(cloudflare): publish unaffected static app routes with config transforms#2510
NathanDrake2406 wants to merge 8 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/static-asset-route-eligibility

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Field Detail
Goal Let the static App Router asset transport from #2506 still publish routes that are not affected by next.config request transforms.
Core change Replace the all-or-nothing config transform skip with a per-route matcher for headers, redirects, and rewrites.
Stack note This is stacked on #2506. Since #2506's base branch exists on a fork, GitHub shows this upstream PR against main until #2506 lands.
Main files for this PR cloudflare-prerender-assets.ts, config-matchers.ts, cloudflare-prerender-assets.test.ts

Why

A request transform is only semantically relevant to a static asset route when that transform can match the same visible route path. If /legacy redirects to /about, that should not prevent publishing /about as a static prerendered asset. The safe boundary is route-local eligibility: affected routes still fall through to the Worker, unaffected routes can keep the static transport win.

Area Principle / invariant What this PR changes
Headers A generated static response must not bypass a header rule that would apply to that same route. Skip only routes whose pathname matches a configured header source.
Redirects A generated static response must not bypass a redirect that would apply to that same route. Skip only routes whose pathname matches a configured redirect source.
Rewrites A generated static response must not bypass a rewrite that would apply to that same route. Skip only routes whose pathname matches beforeFiles, afterFiles, or fallback rewrite sources.
Unrelated config Unrelated request transforms should not disable the whole static transport. Allow unaffected prerendered routes to publish.

What changed

Scenario Before After
App has any header, redirect, or rewrite Static App Router asset publication skipped globally. Each prerendered route is checked against transform sources.
Route path matches a transform source Not published. Still not published.
Route path does not match any transform source Not published because another route had config transforms. Published when the other PR eligibility gates also pass.

Benchmark

Existing benchmark evidence for this route-eligibility delta, captured before the rebase onto the latest #2506 review fixes:

Field Value
Fixture tests/fixtures/cf-app-basic copy without middleware, plus unrelated next.config redirect /legacy -> /about
Runtime local wrangler dev / workerd
Shape 8 alternating AB/BA rounds, 250 warmup, 1600 measured, concurrency 8
Base ee7cfc46e
Head 8325981f5 before rebase, same route-eligibility change now at ee658ed3c
Scenario Base mean ms Head mean ms Diff ms Diff %
document-about-unrelated-redirect 9.1533 4.6008 -4.5525 -49.74%
rsc-about-unrelated-redirect 6.5856 4.5705 -2.0151 -30.60%
route-api-control 4.6233 4.7086 +0.0853 +1.85%

Sanity from the same run:

Asset Base Head
/about HTML asset false true
/about static RSC asset false true

Validation

  • vp fmt packages/vinext/src/build/cloudflare-prerender-assets.ts packages/vinext/src/config/config-matchers.ts tests/cloudflare-prerender-assets.test.ts
  • vp test run tests/app-rsc-cache-busting.test.ts tests/cloudflare-prerender-assets.test.ts tests/cloudflare-rsc-transport-config.test.ts
  • vp check packages/vinext/src/build/cloudflare-static-assets-config.ts packages/vinext/src/build/cloudflare-prerender-assets.ts packages/vinext/src/config/config-matchers.ts packages/vinext/src/index.ts tests/cloudflare-prerender-assets.test.ts tests/cloudflare-rsc-transport-config.test.ts
  • vp run vinext#build

Non-goals

NathanDrake2406 and others added 8 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.
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.
…aders

Static App Router asset publication emitted one _headers rule per HTML and RSC artifact and enabled the client transport from root Wrangler config only. On Workers Static Assets, matching header rules inherit and duplicate values are joined, and selected environments can change assets.not_found_handling.

That made broad user rules able to corrupt static RSC cache headers, pushed publishable apps toward Cloudflare's header-rule limit, and allowed client RSC transport generation to disagree with emitted asset publication.

Resolve Wrangler assets through a shared CLOUDFLARE_ENV-aware helper, fail publication closed when the selected environment disables the transport, collapse static RSC protocol headers to one splat rule, and detach inherited protocol headers before setting generated values.
…ansforms

Cloudflare static App Router asset publication skipped the whole app whenever next.config headers, redirects, or rewrites existed. That was safe, but it made an unrelated /legacy redirect prevent /about from taking the static asset path.

The skipped-app assumption was broader than the routing invariant requires: only a route whose visible pathname can match a request transform must remain Worker-owned.

Reuse config source matching to skip affected routes while publishing routes whose source patterns cannot match. Request-dependent has/missing conditions still fail closed because a matching source skips publication before evaluating request state.
@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@2510
npm i https://pkg.pr.new/vinext@2510

commit: ee658ed

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared ee658ed 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.49 s 2.46 s ⚫ -1.1%
Production build time vinext 3.00 s 3.00 s ⚫ +0.2%
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

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/static-asset-route-eligibility branch from f7ba54b to ee658ed Compare July 3, 2026 16:40
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