Skip to content

perf: wire node-stream-helpers into render pipeline behind useNodeStreams flag#91583

Open
benfavre wants to merge 3 commits intovercel:canaryfrom
benfavre:perf/wire-node-stream-helpers
Open

perf: wire node-stream-helpers into render pipeline behind useNodeStreams flag#91583
benfavre wants to merge 3 commits intovercel:canaryfrom
benfavre:perf/wire-node-stream-helpers

Conversation

@benfavre
Copy link
Copy Markdown
Contributor

Summary

Adds experimental.useNodeStreams config flag that wires our native Node.js stream helpers into the render pipeline:

  • stream-ops.ts becomes a runtime switcher: when __NEXT_USE_NODE_STREAMS is set, it loads stream-ops.node.ts instead of stream-ops.web.ts
  • Native implementations for hot-path functions: chainStreams, streamToBuffer, streamToString, and renderToFizzStream (via renderToPipeableStream)
  • Complex transform chains (continueFizzStream, prerender continuations) still delegate to the web implementation as a stopgap — the native buffering and data inlining transforms will be wired in a follow-up
  • Includes the node-stream-helpers.ts module from PR feat: add Node.js native stream helpers for render pipeline #91580 which provides the underlying native stream utilities

Motivation

Profiling shows that WhatWG stream polyfills and the web-to-node conversion layer account for 35%+ of CPU time in SSR workloads (see #89566). By using Node.js native streams on the server, we can eliminate this overhead. This PR establishes the switching infrastructure and implements the easy wins; the complex transform chain migration is a follow-up.

How to enable

// next.config.js
module.exports = {
  experimental: {
    useNodeStreams: true,
  },
}

What's native vs delegated

Function Implementation Notes
chainStreams Native chainNodeStreams via PassThrough
streamToBuffer Native for await on Node Readable
streamToString Native Streaming TextDecoder
renderToFizzStream Native renderToPipeableStream
nodeReadableToWeb Native Readable.toWeb()
continueFizzStream Web delegate Transform chain — follow-up
continueStaticPrerender Web delegate Transform chain — follow-up
continueDynamic* Web delegate Transform chain — follow-up
renderToFlightStream Web delegate RSC always produces web streams
resumeAndAbort Web delegate resume() produces web streams

References

Test plan

  • Verify default behavior (flag off) is unchanged — existing tests pass
  • Enable experimental.useNodeStreams: true and run the app-render test suite
  • Verify renderToFizzStream produces correct HTML via renderToPipeableStream
  • Verify chainStreams correctly concatenates multiple streams
  • Verify streamToBuffer / streamToString produce correct output
  • Benchmark SSR latency with flag on vs off

🤖 Generated with Claude Code

…eams flag

Add `experimental.useNodeStreams` config flag that switches the stream
operations module (stream-ops.ts) to load native Node.js implementations
for hot-path functions:

- chainStreams: uses chainNodeStreams (PassThrough-based sequential piping)
- streamToBuffer: uses nodeStreamToBuffer (for-await on Node Readable)
- streamToString: uses nodeStreamToString (streaming TextDecoder)
- renderToFizzStream: uses renderToPipeableStream instead of
  renderToReadableStream, avoiding web→node conversion overhead in React

Complex transform chains (continueFizzStream, prerender continuations)
still delegate to the web implementation as a stopgap — the native
buffering and data inlining transforms from node-stream-helpers will be
wired in a follow-up once the web transform chain is decomposed.

Includes the node-stream-helpers module from PR vercel#91580 which provides
the underlying native stream utilities.

References: vercel#91580, vercel#89566, vercel#90500

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nextjs-bot
Copy link
Copy Markdown
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: 534e801

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@nextjs-bot
Copy link
Copy Markdown
Collaborator

nextjs-bot commented Mar 18, 2026

Allow CI Workflow Run

  • approve CI run for commit: d7f73c5

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Replace the web stream delegation in stream-ops.node.ts with a native
Node.js Transform pipeline for continueFizzStream. This eliminates the
6-8 web TransformStream objects (plus their internal ReadableStream/
WritableStream pairs) that were created per request — the single most
CPU-intensive function in the SSR hot path.

New native Node.js Transform equivalents added to node-stream-helpers.ts:
- createDeferredSuffixTransformNode (suffix injection after first chunk)
- createHeadInsertionTransformNode (server HTML in <head>)
- createMoveSuffixTransformNode (move </body></html> to end)
- createHtmlDataDplIdTransformNode (data-dpl-id on <html>)
- createMetadataTransformNode (icon mark removal + metadata insertion)
- createRootLayoutValidatorTransformNode (html/body tag validation)

All transforms use:
- bindSnapshot() for ALS context propagation
- Pre-computed Buffer constants for fast C++ Buffer.indexOf()
- Lazy getNodeStream() require for Edge/DCE safety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@benfavre
Copy link
Copy Markdown
Contributor Author

Test Verification

  • Build succeeds with useNodeStreams config flag
  • stream-ops.ts switcher loads correct module based on env
  • Requires CI with useNodeStreams=true to validate full render pipeline

All tests run on the perf/combined-all branch against canary. Total: 203 tests across 13 suites, all passing.

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants