Skip to content

refactor(ssr): isolate runtime-specific SSR transports#2489

Open
NathanDrake2406 wants to merge 15 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/node-ssr-renderer-transform
Open

refactor(ssr): isolate runtime-specific SSR transports#2489
NathanDrake2406 wants to merge 15 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/node-ssr-renderer-transform

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Overview

Field Details
Goal Make SSR renderer transport selection explicit for Node and Web-compatible builds.
Core change App and Pages SSR now choose a Node or Web render transport at entry generation while sharing lifecycle orchestration.
Boundary SsrRenderTransport selects renderer and stream primitives, not deployment ownership.
Primary review path packages/vinext/src/entries/*, packages/vinext/src/server/app-ssr-entry-core.ts, packages/vinext/src/server/app-ssr-render-runtime.*.ts, packages/vinext/src/server/pages-render-runtime.*.ts, stream tests.
Expected impact Cleaner runtime seam, safer Worker bundles, Node Fizz transport available for plain Node SSR.

Why

SSR orchestration should stay shared, but renderer and stream primitives are runtime-specific. Plain Node can use React's Node Fizz transport, while Cloudflare, Nitro, and other Web-compatible targets need Web Stream compatible rendering. Encoding that as a transport seam keeps platform-specific dependencies out of shared lifecycle code.

This refactor is the first layer of that boundary, not a final Node server rewrite. Today the production handler still returns the existing Web Response, so Node builds cross back through the current response boundary. Splitting the renderer transport first makes the lower-level runtime difference explicit and testable before any future work changes the Node server handoff itself.

Area Principle / invariant What this PR changes
Entry generation Build output should choose the transport once. Resolves SsrRenderTransport from Cloudflare/Nitro presence and threads it into App and Pages entries.
App SSR Lifecycle orchestration should not fork by platform. Keeps shared App SSR orchestration in core code and moves only renderer transport into Node/Web runtime modules.
Pages SSR Generated entries should stay routing/config glue. Moves Pages HTML rendering behind Node/Web runtime helpers.
HTML insertion Boundary-sensitive insertion logic should not drift. Shares one target-neutral App HTML insertion state machine across Web and Node stream adapters.
Worker safety Node renderer internals must not leak into Worker bundles. Adds Cloudflare fixture bundle assertions for Node renderer transport non-leakage.

What changed

Surface Before After
App SSR Generated entry re-exported one Web renderer path. Generated entry selects app-ssr-entry.node or app-ssr-entry.web.
Pages SSR Generated entry owned Web-stream render helpers. Generated entry imports pages-render-runtime.node or pages-render-runtime.web.
Node SSR Node paid Web renderer and string-drain costs inside owned render transport. Node transport uses Node Fizz streams and converts back to the existing Web Response boundary.
Web/Workers SSR Shared path risked accumulating Node-specific branches. Web transport keeps react-dom/server.edge and Web Streams isolated.
Cancellation Direct destination close was covered, but the real transformed/Web topology needed proof. Topology test covers React destination to Node transform to Readable.toWeb() cancellation.
Maintainer review path
  1. packages/vinext/src/index.ts and packages/vinext/src/entries/ssr-render-transport.ts for the single transport selection rule.
  2. packages/vinext/src/entries/app-ssr-entry.ts and packages/vinext/src/entries/pages-server-entry.ts for generated entry selection.
  3. packages/vinext/src/server/app-ssr-entry-core.ts for the shared App SSR orchestration interface.
  4. packages/vinext/src/server/app-ssr-render-runtime.node.ts and .web.ts for App renderer transport differences.
  5. packages/vinext/src/server/app-ssr-stream.ts and app-ssr-stream-node.ts for shared insertion state and thin stream adapters.
  6. packages/vinext/src/server/pages-render-runtime.node.ts and .web.ts for Pages render transport selection.
  7. tests/node-render-runtime.test.ts, tests/prerender.test.ts, and stream tests for bundle and cancellation boundaries.
Validation
  • vp check packages/vinext/src/entries/app-ssr-entry.ts packages/vinext/src/entries/pages-server-entry.ts packages/vinext/src/entries/ssr-render-transport.ts packages/vinext/src/index.ts tests/node-render-runtime.test.ts tests/prerender.test.ts
  • vp run knip
  • vp test run tests/node-render-runtime.test.ts tests/entry-templates.test.ts tests/app-router-next-config-codegen.test.ts
  • vp test run tests/prerender.test.ts -t "Cloudflare Workers hybrid build"
  • Earlier branch coverage: vp test run tests/node-fizz-stream.test.ts tests/app-ssr-stream.test.ts tests/rsc-streaming.test.ts

Note: repo-wide vp check currently fails outside this PR on missing @cloudflare/kumo/components/table types in apps/web/app/benchmarks/.... Touched-file checks, targeted runtime tests, the Cloudflare fixture test, and knip pass.

Risk / compatibility
  • Public API: no intended public API change.
  • Build output: plain Node builds now emit Node renderer transport imports for owned SSR HTML rendering.
  • Workers: Cloudflare fixture asserts the Node renderer transport does not leak into the Worker bundle.
  • Runtime boundary: production still returns the existing Web Response boundary.
  • RSC seam: RSC deserialization remains Web-stream based through @vitejs/plugin-rsc/ssr.
Non-goals
  • This is not a full native IncomingMessage -> ServerResponse rewrite.
  • This does not replace the public Web Request/Response production boundary.
  • This does not change RSC deserialization away from the current Web-stream seam.
  • This does not claim a broad end-to-end latency win.
  • A future Node-server layer could remove the remaining Web Response handoff in vinext start; this PR deliberately stops at the SSR renderer transport seam.

Node and Web SSR renderer selection is a transport choice, not a deployment runtime target. Naming the shared type as a transport makes the Cloudflare and Nitro mapping clearer while keeping the .web runtime modules generic.\n\nAdd committed bundle assertions for the two main boundary promises: plain Node App builds use the Node renderer transport, and Cloudflare Worker output does not include the Node renderer transport modules.
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 19a24c8

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 19a24c8 against base 5471b41 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.4 KB ⚫ +0.0%
Client entry size (gzip) vinext 120.5 KB 120.5 KB ⚫ +0.0%
Dev server cold start vinext 2.60 s 2.61 s ⚫ +0.3%
Production build time vinext 3.20 s 3.16 s ⚫ -1.2%
RSC entry closure size (gzip) vinext 98.0 KB 98.0 KB ⚫ 0.0%
Server bundle size (gzip) vinext 164.5 KB 165.2 KB ⚫ +0.4%

View detailed results and traces

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

…derer-transform

# Conflicts:
#	packages/vinext/src/entries/pages-server-entry.ts
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