Web Streams TransformStream 如何处理背压
来源:17golang原创
时间:2026-09-15 08:08:53 361浏览 收藏
前端处理大文件、网络响应或实时数据时,最容易出现的症状是生产端不断 enqueue,消费端却来不及读取,结果内存队列越来越长。TransformStream 的背压不是另加一个“限速开关”,而是由 readable 侧的队列状态影响 writable 侧是否继续接受写入,再沿着 pipeThrough 管道传回上游。
官方资料:https://developer.mozilla.org/en-US/docs/Web/API/TransformStream
- 自动管道优先用
readable.pipeThrough(transform).pipeTo(writable),让下游能力决定上游节奏。 controller.desiredSize观察 TransformStream 的 readable 队列,writer.desiredSize和writer.ready观察 writable 队列。- highWaterMark 只改变“何时施加压力”的阈值,不会让慢消费者凭空变快。
先画清 writable、transform 和 readable 的背压方向
TransformStream 可以看成一条中间管道:输入写入 writable,transform(chunk, controller)产生输出,消费者从 readable读取。下游读取速度变慢时,readable 内部队列接近高水位线,TransformStream 的 writable 写入会等待;如果它前面还有 ReadableStream,压力会继续向前传播。
这里有两个容易混淆的观察点。transform 回调里的 controller.desiredSize表示关联 readable 队列还希望接收多少大小;手动取得 writable writer 后,writer.desiredSize表示 writable 队列距离高水位线还有多少空间。数值小于或等于零时,应把它当作“继续写入会积压”的信号,而不是把它当作已经丢数据。

用 pipeThrough 建立自动背压管道
如果只是把数据从来源变换后交给另一个 WritableStream,优先让 Streams API 管理等待关系。pipeThrough()把 TransformStream 放进管道,pipeTo()负责连接最终写入端;当最终写入端返回一个尚未完成的写入 Promise,前面的读取就会自然放慢。
const upperCase = new TransformStream({
transform(chunk, controller) {
// 只转换当前分块;enqueue 会把结果放入 readable 侧队列。
controller.enqueue(String(chunk).toUpperCase());
}
});
const slowSink = new WritableStream({
async write(chunk) {
// 用延迟模拟下游处理较慢;真实项目中这里可能是文件或网络写入。
await new Promise(resolve => setTimeout(resolve, 20));
console.log(chunk);
}
});
// pipeThrough 保留 TransformStream 的两端,pipeTo 连接最终消费者。
await source.pipeThrough(upperCase).pipeTo(slowSink);
这段示例的关键不是延迟值,而是没有在生产循环里无条件把所有 chunk 先放进数组。若 source 是自定义 ReadableStream,还应在 pull() 中依据 controller 的 desiredSize 决定是否继续准备数据。管道完成时用 pipeTo() 返回的 Promise 作为整体成功信号。
手动写入时等待 writer.ready
上传分片、逐块解码或需要自己掌控生命周期时,可以直接获取 TransformStream 的 writable writer。此时不要只打印一次 desiredSize就继续写;更可靠的做法是写入一块后,在队列进入压力区时等待 writer.ready。该 Promise 会在队列恢复到可接受状态时解决。

const writer = transform.writable.getWriter();
try {
for (const chunk of chunks) {
// write 返回的 Promise 表示这一块已交给 writable 侧处理。
await writer.write(chunk);
// desiredSize 非正时说明队列有压力,等待 ready 再继续。
if (writer.desiredSize
writer.ready是等待“背压解除”的信号,不是所有业务处理完成的信号;真正的整体完成仍要等待 readable 被消费完或外层管道 Promise 结束。若 write、ready 或 close 抛错,应把它当作错误/关闭路径处理,不要用 while 循环忙等。
检查高水位线、读取端和验证信号
背压看起来“不生效”时,按下面三层检查,通常比单纯调大 highWaterMark 更快定位:
| 检查点 | 看什么 | 结论 |
|---|---|---|
| 队列阈值 | readableStrategy.highWaterMark、writableStrategy.highWaterMark和 size() | 阈值改变只影响触发压力的时机,size 不匹配会让“一个 chunk”并不等于一字节。 |
| 消费路径 | 最终 writable 的 write()是否真的等待 I/O | 如果下游立即 resolve,背压很快解除;若业务另有数组缓存,内存仍会增长。 |
| 状态信号 | desiredSize、writer.ready、pipeTo()的 resolve/reject | 区分暂时积压、正常关闭和错误终止,不能把负数当成丢包证据。 |
最后做一次反向验证:让消费端人为变慢,观察生产端是否在若干块后出现等待;再恢复消费速度,确认 writer.ready能够继续推进。这个验证只说明管道的节奏受下游影响,并不代表所有数据源都能被暂停,已经进入外部 SDK 或不可控回调的缓存仍需单独治理。
相关问题
把 highWaterMark 调大是不是就能解决内存上涨?
不能。它只扩大队列偏好的容量,可能延后背压触发;如果消费者持续慢,积压仍会增长,应先确认消费路径和 chunk 的 size 计算。
controller.desiredSize 和 writer.desiredSize 能互相替代吗?
不能。前者观察 transform 输出的 readable 队列,后者观察手动写入的 writable 队列。自动 pipe 链路通常无需手工读取它们,只有自定义 source 或直接 writer 写入时才需要据此做额外控制。
记住一条判断线:让数据经过可传播背压的 Streams 管道,优先等待 Promise;只有在自己生产或写入时,才用 desiredSize 判断压力、用 ready 等待恢复,并在 close、abort、cancel 的生命周期上做完整收尾。
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
153 收藏
-
150 收藏
-
465 收藏
-
136 收藏
-
325 收藏
-
475 收藏
-
419 收藏
-
209 收藏
-
485 收藏
-
173 收藏
-
220 收藏
-
339 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习