处理视频流时突然卡顿?处理大文件时内存爆表?这些看似奇怪的问题,可能源于 JavaScript 中一个被广泛采用但设计复杂的标准 API——Web Streams。当你的 Node.js 应用突然因为未消费的 body 耗尽连接池,或者处理大文件时内存爆表,你可能已经踩过 Web Streams 的坑了。
问题:Web Streams 的设计缺陷
Web Streams 是 JavaScript 中处理数据流的标准 API,2014-2016 年设计,旨在统一浏览器和服务器的数据流处理。它被用于 fetch()、Node.js、Cloudflare Workers 等场景,成为现代 Web 应用的数据传输基础。WHATWG Streams Standard 文档 定义了这套机制,初衷是让开发者能以统一方式处理实时数据、大文件、网络请求等流式场景。然而,经过多年的实际使用,开发者们发现这个标准 API 存在诸多问题。
例如,读取流数据需要繁琐的锁管理:const reader = stream.getReader(); 和 reader.releaseLock();,一旦忘记释放锁,整个流就永久锁死。更严重的是,response.clone() 这样的 API 会隐式创建分支流,如果不消费所有分支,会导致连接池耗尽。Matteo Collina(Node.js 技术委员会主席)在讨论中指出:「Cloning streams in Node.js's fetch() implementation is harder than it looks… the coordination required between two readers sharing one source makes it easy to accidentally break the original request or exhaust connection pools.」(在 Node.js 的 fetch() 实现中克隆流比看起来困难得多……共享单个源的两个读取器之间需要协调,这很容易意外破坏原始请求或耗尽连接池。)
BYOB(自带缓冲区)机制本为优化内存,但实际使用复杂:需要单独的 ReadableStreamBYOBReader,处理缓冲区转移,且几乎不被使用。背压机制也形同虚设——desiredSize 只是建议值,生产者可以无视地持续写入,导致内存无限增长。Vercel 的 研究 显示,Node.js 中 Web Streams 管道性能比优化后方案低 12 倍,主要问题在于「Promise 和对象分配开销」。
新方案:原生异步迭代流
一位 Cloudflare 工程师提出新方案:将流设计为原生 async iterable,直接通过 for await...of 消费,无需锁管理。数据仅在消费时处理(pull-through),批量处理 Uint8Array[] 减少 Promise 开销,同步/异步分离路径避免无谓开销。例如,创建和消费流的代码从:
const { readable, writable } = new TransformStream();
const enc = new TextEncoder();
const writer = writable.getWriter();
await writer.write(enc.encode("Hello, World!"));
await writer.close();
writer.releaseLock();
const dec = new ();
text = ;
( chunk readable) {
text += dec.(chunk, { : });
}
text += dec.();


