S
SkillNav

我们把 WebStreams“Ralph Wiggum 化”,性能提升最高 14.6 倍

深度2026-02-18T13:00:00+00:0011 分钟阅读
我们把 WebStreams“Ralph Wiggum 化”,性能提升最高 14.6 倍

9 分钟阅读

2026 年 2 月 18 日

今年早些时候,我们开始对 Next.js 服务端渲染做性能剖析时,有一个东西在火焰图里反复出现:WebStreams。不是流里跑的应用代码,而是流本身。Promise 链、每个 chunk 的对象分配、微任务队列跳转。在 Theo Browne 的服务端渲染基准测试指出框架开销占用了大量计算时间之后,我们开始追踪这些时间到底花在了哪里。很大一部分都在 streams 上。

结果发现,WebStreams 有一套极其完整的测试集,这让它非常适合用 AI 做“基于测试驱动 + 基准驱动”的重实现。本文会讲我们做了哪些性能优化、踩了哪些坑,以及这些工作如何已经通过 Matteo Collina 的上游 PR进入 Node.js 本体。

Link to heading问题所在

Node.js 有两套流 API。老一代(stream.Readablestream.Writablestream.Transform)已经存在十多年,优化非常充分。数据走的是 C++ 内部路径,背压是一个布尔值,管道连接就是一次函数调用。

新一代是 WHATWG Streams API:ReadableStreamWritableStreamTransformStream。这是 Web 标准,支撑了 fetch() 响应体、CompressionStreamTextDecoderStream,以及越来越多框架(如 Next.js、React)的服务端渲染。

向 Web 标准收敛是正确方向。但在服务端,它比应有的速度更慢。

要理解原因,可以看在 Node.js 里调用原生 WebStream 的 reader.read() 时发生了什么。即使数据已经在缓冲区里:

  1. 会分配一个带 3 个回调槽位的 ReadableStreamDefaultReadRequest 对象
  2. 这个 request 会被入队到流的内部队列
  3. 新建并返回一个 Promise
  4. 最终解析还要走一遍微任务队列

也就是说,为了返回“本来就在那里”的数据,要付出 4 次分配 + 1 次微任务跳转。再把这个成本乘以渲染流水线里每个 transform 上流经的每个 chunk。

再看 pipeTo():每个 chunk 都会经过完整 Promise 链:read、write、检查背压、再来一轮。每次 read 还会分配一个 {value, done} 结果对象。错误传播会再产生额外的 Promise 分支。

这些设计本身并没错。在浏览器里这些保证很重要:流跨越安全边界、取消语义必须严密、pipe 两端并不在你控制范围内。但在服务端,如果你在 1KB chunk 粒度下,把 React Server Components 通过 3 层 transform 管道传输,成本就会迅速累积。

我们测得原生 WebStream pipeThrough 在 1KB chunk 下吞吐是 630 MB/s。同样的 passthrough transform,用 Node.js pipeline() 可以到 ~7,900 MB/s。差了 12 倍,而且几乎全是 Promise 和对象分配带来的开销。

Link to heading我们做了什么

我们一直在做一个叫 fast-webstreams 的库:对外实现 WHATWG 的 ReadableStreamWritableStreamTransformStream API,内部则用 Node.js streams 做承载。API 一样、错误传播一样、规范兼容性一样,但把常见路径上的额外开销去掉了。

核心思路是:根据实际操作类型,走不同的快路径。

Link to heading快流之间 pipe:每个 chunk 零 Promise

这是最大的收益点。当你在 fast streams 之间链式调用 pipeThrough / pipeTo 时,库不会立刻开始传输,而是先记录上游链路:

source → transform1 → transform2 → ...

当链尾调用 pipeTo() 时,它会向上回溯,收集底层 Node.js stream 对象,然后只发起一次 pipeline()。一次函数调用,每个 chunk 零 Promise,数据直接走 Node 已优化的 C++ 路径。

code
const source = new ReadableStream({  pull(controller) {    controller.enqueue(generateChunk());  }});const transform = new TransformStream({  transform(chunk, controller) {    controller.enqueue(process(chunk));  }});const sink = new WritableStream({  write(chunk) { consume(chunk); }});// Internally: single pipeline() call, zero promises per chunk
await source.pipeThrough(transform).pipeTo(sink);

结果:~6,200 MB/s。比原生 WebStreams 快约 10 倍,并且接近原始 Node.js pipeline 性能。

如果链中有任意一个流不是 fast stream(例如原生 CompressionStream),库会回退到原生 pipeThrough 或规范兼容的 pipeTo 实现。

Link to heading逐 chunk 读取:同步解析

调用 reader.read() 时,库会先同步尝试 nodeReadable.read()。有数据就直接 Promise.resolve({value, done});不走事件循环往返,不分配 request 对象。只有缓冲区为空时,才注册 listener 并返回 pending Promise。

code
const reader = stream.getReader();while (true) {  const { value, done } = await reader.read();  if (done) break;  // When data is buffered, the await resolves immediately  // via Promise.resolve() — no microtask queue hop  processChunk(value);}

结果:~12,400 MB/s,约是原生的 3.7 倍。

Link to headingReact Flight 模式:差距最大的场景

这对 Next.js 最关键。React Server Components 使用的是一种特定字节流模式:创建 type: 'bytes'ReadableStream,在 start() 里拿到 controller,然后随着渲染推进从外部 enqueue。

code
let ctrl;const stream = new ReadableStream({  type: 'bytes',  start(c) { ctrl = c; }});// As React renders each component:
ctrl.enqueue(new Uint8Array(payload1));ctrl.enqueue(new Uint8Array(payload2));ctrl.close();

原生 WebStreams:~110 MB/s。fast-webstreams:~1,600 MB/s。也就是 14.6 倍,而且正是生产环境服务端渲染在用的模式。

提速来自我们写的 LiteReadable:一个最小化的数组缓冲实现,用来替代 Node.js Readable 处理字节流。它用直接回调分发代替 EventEmitter,支持 pull-based demand 和 BYOB reader,单次构造成本还能再低约 5 微秒。React Flight 每个请求会创建数百个字节流,这点优化就很关键。

Link to headingfetch 响应体:你无法自行构造的流

上面的例子都从 new ReadableStream(...) 开始。但在服务端,大多数流其实来自 fetch()。响应体是 Node.js HTTP 层持有的原生字节流,你不能直接替换。

这是 SSR 很常见的路径:从上游服务拉数据,通过一个或多个 transform,再转发给客户端。

code
const upstream = await fetch('<https://api.example.com/data>');// Pipe through transforms and forward as a new Response
const transformed = upstream.body  .pipeThrough(new TransformStream({ transform(chunk, ctrl) { /* ... */ ctrl.enqueue(chunk); } }))  .pipeThrough(new TransformStream({ transform(chunk, ctrl) { /* ... */ ctrl.enqueue(chunk); } }))  .pipeThrough(new TransformStream({ transform(chunk, ctrl) { /* ... */ ctrl.enqueue(chunk); } }));return new Response(transformed);

原生 WebStreams 下,链路每一跳都要付出每 chunk Promise 的全套成本。3 个 transform 大致就是每 chunk 6~9 个 Promise。1KB chunk 时吞吐约 ~260 MB/s

这个库通过“延迟解析”处理该问题:启用 patchGlobalWebStreams() 后,Response.prototype.body 返回的是包裹原生字节流的轻量 fast shell。调用 pipeThrough() 不会立刻开始传输,只记录链路;直到链尾调用 pipeTo()getReader(),才一次性解析整条链:在原生 reader 到 Node.js pipeline() 之间建立单桥接,再从输出缓冲同步读取。

成本模型是:原生边界拉数据时 1 个 Promise;transform 链内部每 chunk 0 Promise;输出端同步 read。

结果:3-transform 的 fetch 模式达到 ~830 MB/s,比原生快 3.2 倍。若只是简单响应转发(无 transform),也有 2.0 倍(850 vs 430 MB/s)。

Link to heading基准测试

所有数据均为 Node.js v22、1KB chunk 下的吞吐(MB/s),数值越高越好。

Link to heading核心操作

Operation

Node.js streams

fast

native

fast vs native

read loop

26,400

12,400

3,300

3.7x

write loop

26,500

5,500

2,300

2.4x

pipeThrough

7,900

6,200

630

9.8x

pipeTo

14,000

2,500

1,400

1.8x

for-await-of

4,100

3,000

1.4x

Link to headingTransform 链

每 chunk Promise 开销会随着链深叠加:

Depth

fast

native

fast vs native

3 transforms

2,900

300

9.7x

8 transforms

1,000

115

8.7x

Link to heading字节流

Pattern

fast

native

fast vs native

start + enqueue (React Flight)

1,600

110

14.6x

byte read loop

1,400

1,400

1.0x

byte tee

1,200

750

1.6x

Link to heading响应体模式

Pattern

fast

native

fast vs native

Response.text()

900

910

1.0x

Response forwarding

850

430

2.0x

fetch → 3 transforms

830

260

3.2x

Link to heading流构造

流创建速度也更快,这对短生命周期流很重要:

Type

fast

native

fast vs native

ReadableStream

2,100

980

2.1x

WritableStream

1,300

440

3.0x

TransformStream

470

220

2.1x

Link to heading规范兼容性

fast-webstreams 通过了 1,116 项 Web Platform Tests 中的 1,100 项。Node.js 原生实现通过 1,099 项。剩余 16 项失败要么是原生同样存在的问题(例如尚未实现的 type: 'owning' 传输模式),要么是不会影响真实应用的架构差异。

Link to heading我们如何落地部署

该库可直接 patch 全局 ReadableStreamWritableStreamTransformStream 构造器:

code
import { patchGlobalWebStreams } from 'fast-webstreams';
patchGlobalWebStreams();// globalThis.ReadableStream is now the fast implementation// fetch() response bodies are automatically wrapped// All downstream pipeThrough/pipeTo use fast paths

这个 patch 还会拦截 Response.prototype.body,把原生 fetch 响应体包裹成 fast stream shell,让 fetch()pipeThrough()pipeTo() 链路自动命中 pipeline 快路径。

在 Vercel,我们正在评估将其逐步推广到整个集群。推进会谨慎且渐进。流原语位于请求处理、响应渲染、压缩等基础路径。我们会先覆盖差距最大的模式:React Server Component 流式传输、响应体转发、多级 transform 链,并先在生产环境观测,再扩大范围。

Link to heading正确的修复方向是上游

长期来看,用户态库不该是最终答案。真正的修复应在 Node.js 本身。

相关工作已经开始。我们在 X 上交流后,Matteo Collina 提交了 nodejs/node#61807:“stream: add fast paths for webstreams read and pipeTo”。这个 PR 将本项目的两个思路直接应用到 Node.js 原生 WebStreams:

  1. read() 快路径:当数据已在缓冲区时,直接返回 resolved Promise,不再创建 ReadableStreamDefaultReadRequest 对象。这依旧符合规范,因为 read() 无论如何都返回 Promise,而 resolved Promise 的回调仍在微任务队列执行。
  2. pipeTo() 批量读取:当数据已缓冲时,从 controller 队列批量读取,避免每个 chunk 分配 request 对象;每次 write 后检查 desiredSize,确保背压语义不被破坏。

PR 显示:缓冲读取快 ~17–20%pipeTo~11%。这些改进将让所有 Node.js 用户“零成本”受益:无需安装库、无需 patch、风险更低。

James Snell 的 Node.js performance issue #134 还列出了更多机会:内部来源流走 C++ 级别 piping、lazy buffering、消除 WritableStream 适配器中的双缓冲。每一项都可能进一步缩小差距。

我们会继续向上游贡献思路。目标不是让 fast-webstreams 永远存在,而是让 WebStreams 快到不再需要它。

Link to heading我们踩坑后得到的经验

规范比看上去更“聪明”。 我们尝试过很多捷径,几乎每一条都会打破某个 WPT,而测试通常是对的。ReadableStreamDefaultReadRequest 模式、每次读取都返回 Promise 的设计、严密的错误传播机制——这些都不是偶然,它们是为了解决真实边界情况:读取中取消、锁定流中的错误标识一致性、thenable 拦截等,真实代码确实会触发。

Promise.resolve(obj) 总会检查 thenable。 这是语言级行为,绕不开。如果 resolve 的对象带 .then 属性,Promise 机制就会调用它。有些 WPT 会故意给 read 结果挂 .then 并验证实现是否正确处理。我们因此必须非常谨慎地决定 {value, done} 在热路径中的创建位置。

Node.js 的 pipeline() 不能直接替代 WHATWG 的 pipeTo 我们原本希望所有 pipe 都统一走 pipeline(),结果导致 72 项 WPT 失败。错误传播、流锁定、取消语义在两者之间本质不同。只有在我们完全控制整条链时 pipeline() 才安全,这也是我们“先收集上游链接、仅对纯 fast-stream 链使用它”的原因。

要用 Reflect.apply,不要用 .call() WPT 会 monkey-patch Function.prototype.call,并验证实现不会用它调用用户回调。安全做法只有 Reflect.apply。这是实打实的规范要求。

Link to heading我们用 AI 完成了 fast-webstreams 的大部分实现

有两件事让这成为可能:

The amazing [W

[... 内容截断 ...]

原文链接:https://vercel.com/blog/we-ralph-wiggumed-webstreams-to-make-them-10x-faster

相关文章

AINews:Harness Engineering 到底是不是一门真学问?
深度·3月5日
AINews:Harness Engineering 到底是不是一门真学问?

这篇文章围绕 AI 工程中的核心争议展开:系统能力究竟主要来自更强的模型(Big Model),还是来自更强的编排层(Big Harness)。文中汇总了 OpenAI、Anthropic、Scale AI、METR 等多方观点与数据,显示两派在“模型进步会不会吞噬 Harness 价值”上分歧明显。作者最终认为,随着 Agent 产品落地加速,Harness Engineering 的独立价值正在被市场和社区进一步确认。

10 分钟
每个 Agent 都需要一个 Box:Aaron Levie 谈 AI 时代的新基础设施
深度·3月5日
每个 Agent 都需要一个 Box:Aaron Levie 谈 AI 时代的新基础设施

在围绕“AI 是否正在杀死 SaaS”的争论中,Box CEO Aaron Levie 提出相反观点:企业内容与文件系统在 Agent 时代反而更关键。随着 Filesystem、Sandbox 和 Agent 工作流快速普及,核心问题从“让 Agent 能做事”转向“如何治理 Agent 的身份、权限与安全边界”。他认为,未来企业将拥有远多于人的 Agent 数量,而真正的竞争力在于率先完成面向 Agent 的组织与基础设施改造。

8 分钟