Node.js streams: Fork points (#92252)
### What?
Implements native Node.js stream support for the RSC / HTML rendering.
This first step does not include cacheComponents / static generation.
Instead of converting between Node.js Readable streams and Web
ReadableStreams at every boundary, the rendering pipeline now uses
native Node.js `Transform`/`PassThrough`/`Readable` streams end-to-end
when the useNodeStreams experimental flag is enabled.
### Why?
The current rendering pipeline has a bottleneck on Web Streams. Each
step adds overhead: buffering, scheduling mismatches, and unnecessary
memory copies. Node.js streams are the native I/O primitive on the
server. Using them directly eliminates these conversion and
scheduling/promise costs.
This is the foundational plumbing needed to run the full app render
pipeline on native Node.js streams, which improves server-side rendering
throughput.
### How?
**Stream transform reimplementation** (`stream-ops.node.ts`):
All web `TransformStream`-based transforms have been reimplemented as
Node.js `Transform` streams:
- `createBufferedTransformStream` — coalesces chunks written in the same
microtask into a single `Uint8Array`
- `createFlightDataInjectionTransform` — interleaves RSC flight data
chunks with the HTML stream
- `createHeadInsertionTransform` — inserts server-generated HTML
(polyfills, scripts) before `</head>`
- `createMetadataTransform` — finds and replaces the `«nxt-icon»` meta
mark
- `createDeferredSuffixTransform` — appends the document suffix after
the first HTML chunk
- `createMoveSuffixTransform` — moves `</body></html>` to the end of the
stream
- `createHtmlDataDplIdTransform` — inserts a `data-dpl-id` attribute on
the opening `<html>` tag
- `createRootLayoutValidatorTransform` — validates `<html>` and `<body>`
tags are present (dev only)
**`ReplayableNodeStream`** (`app-render-prerender-utils.ts`):
New class that buffers all chunks from a source `Readable` and allows
creating multiple independent replay streams via `createReplayStream()`.
This replaces the web stream `tee()` operation for `ReactServerResult`
when using Node.js streams. Uses pull-based delivery (`_read()`) to
avoid AsyncLocalStorage context issues — chunks are only delivered when
the consumer reads, keeping them inside the correct ALS scope during
Fizz's `performWork`. Includes 392 lines of unit tests covering
buffering, live replay, error propagation, multiple independent replays,
subscriber cleanup, and dispose behavior.
**Native rendering functions**:
- `renderToNodeFlightStream` — calls React's `renderToPipeableStream`
directly, pipes to a `PassThrough`
- `renderToNodeFizzStream` — calls `renderToPipeableStream` for HTML
rendering without the web stream wrapper
- `createNodeInlinedDataStream` — wraps RSC flight data in `<script>`
tags using `Buffer` operations and `isUtf8()` for encoding detection
**`continueFizzStream` rewritten for Node.js**:
Instead of converting to web streams and delegating to
`webContinueFizzStream`, the Node.js path pipes through a chain of
native `Transform` streams directly: buffer → dpl-id → metadata →
deferred suffix → flight data injection → root layout validator → move
suffix → head insertion.
**API renames for clarity**:
- `renderToFlightStream` → `renderToWebFlightStream` /
`renderToNodeFlightStream`
- `renderToFizzStream` → `renderToWebFizzStream` /
`renderToNodeFizzStream`
- `createInlinedDataStream` → `createWebInlinedDataStream` /
`createNodeInlinedDataStream`
- `createDebugChannel` → `createWebDebugChannel` /
`createNodeDebugChannel`
**`app-render.tsx` fork points**:
The main render function now branches on
`process.env.__NEXT_USE_NODE_STREAMS` at two key points:
1. RSC rendering — uses `renderToNodeFlightStream` +
`createNodeDebugChannel`
2. HTML rendering — uses `renderToNodeFizzStream` + native
`continueFizzStream` with `createNodeInlinedDataStream`
All pre-render, cache components, and dev staged-render paths continue
to use web streams.
**CI updates** (`.github/workflows/build_and_test.yml`):
The `use-node-streams` test jobs now run without
`__NEXT_CACHE_COMPONENTS` / `__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS` /
`__NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER` to isolate the Node.js
streams path.