登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

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.desiredSizewriter.ready观察 writable 队列。
  • highWaterMark 只改变“何时施加压力”的阈值,不会让慢消费者凭空变快。

先画清 writable、transform 和 readable 的背压方向

TransformStream 可以看成一条中间管道:输入写入 writabletransform(chunk, controller)产生输出,消费者从 readable读取。下游读取速度变慢时,readable 内部队列接近高水位线,TransformStream 的 writable 写入会等待;如果它前面还有 ReadableStream,压力会继续向前传播。

这里有两个容易混淆的观察点。transform 回调里的 controller.desiredSize表示关联 readable 队列还希望接收多少大小;手动取得 writable writer 后,writer.desiredSize表示 writable 队列距离高水位线还有多少空间。数值小于或等于零时,应把它当作“继续写入会积压”的信号,而不是把它当作已经丢数据。

TransformStream 中 writable、transform、readable 与下游消费队列的静态关系示意图
图1:背压结构示意图。重点看“输出队列”和“下游消费”两个分组,压力会从 readable 侧反向影响 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 会在队列恢复到可接受状态时解决。

手动写入 TransformStream 时通过 writer desiredSize 和 ready 控制生产的静态关系示意图
图2:手动写入关系示意图。写入者只在 writable 队列允许时继续,ready 解决后再生产下一批数据。
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.highWaterMarkwritableStrategy.highWaterMarksize()阈值改变只影响触发压力的时机,size 不匹配会让“一个 chunk”并不等于一字节。
消费路径最终 writable 的 write()是否真的等待 I/O如果下游立即 resolve,背压很快解除;若业务另有数组缓存,内存仍会增长。
状态信号desiredSizewriter.readypipeTo()的 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 的生命周期上做完整收尾。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>