← 中文资源

简体中文版本

Fetch 流式响应与背压控制

用手动 reader 循环消费 fetch 响应流:逐块读取累计进度,消费速度慢时读取节奏自然放慢,把背压传导给上游。

阅读英文原文

示例说明

await res.arrayBuffer() 会把整个响应缓冲进内存,下载几 GB 文件必然崩溃。getReader 的手动循环每次只持有一个分块,调用 read 的时机完全由消费速度决定——下游处理慢,read 就调用得慢,TCP 窗口随之收缩,压力沿链路反向传导到服务端,这就是浏览器侧的背压机制。

技术要点

  • reader 循环每次只驻留一个分块
  • content-length 支撑百分比进度回调
  • 消费延迟自动转化为上游读取减速

代码解读

循环里把每个 Uint8Array 存入 parts 累计已收字节;进度回调在总长已知时换算百分比;chunkPauseMs 模拟慢消费者,验证读取节奏随之放慢。结束后 Blob 组装全部分块,received 与 total 对账校验完整性。

使用建议

真正落盘场景应配合流式写入而非累积 Blob。中途取消调用 reader.cancel 让连接释放。进度回调避免过度频繁触发 UI 更新,可按阈值节流。

代码示例

// stream the response body incrementally so backpressure applies
async function downloadWithProgress(url, onProgress, chunkPauseMs = 0) {
  const res = await fetch(url);
  if (!res.ok || !res.body) throw new Error('download failed: ' + res.status);

  const total = Number(res.headers.get('content-length') ?? 0);
  const reader = res.body.getReader();
  let received = 0;
  const parts = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    parts.push(value);
    received += value.length;

    if (onProgress && total > 0) {
      onProgress(received, total);
    }
    if (chunkPauseMs > 0) {
      // 消费变慢时读取循环自然放慢,读取暂停即向上游传导背压
      await new Promise((r) => setTimeout(r, chunkPauseMs));
    }
  }

  const blob = new Blob(parts);
  return { bytes: received, total, blob };
}

const result = await downloadWithProgress(
  'https://cdn.example.com/dataset.bin',
  (got, total) => console.log('progress:', Math.round((got / total) * 100) + '%'),
  10,
);
console.log('downloaded', result.bytes, 'of', result.total, 'bytes');