Compression Streams API:浏览器边界
使用 CompressionStream 和 DecompressionStream 处理 gzip 或 deflate,并明确 backpressure、取消与数据限制。
BotBrowser Team
Compression Streams API 可以在不把全部数据放进一个 JavaScript buffer 的情况下压缩或解压 Web stream。当响应体、文本解码器、取消信号和服务器 Content-Encoding 共用一条管线时,边界就变得重要。BotBrowser 支持复现获授权的 fixture,并记录可见的成功、取消和错误状态;它不能认证远程服务器的压缩策略或密码学完整性。需要延迟分析时参考浏览器性能测量,需要跨 runtime 的二进制能力时参考 WebAssembly 可移植性。
TL;DR
CompressionStream和DecompressionStream使用gzip、deflate或deflate-raw转换字节,但不验证应用语义。ReadableStream只有在消费者施加 backpressure、所有者取消自己创建的分支时才会限制内存。- 解压后再解码文本,保持字节所有权,并把无效输入作为明确错误处理。
- 浏览器支持、服务器
Content-Encoding和数据完整性是三个不同问题。
目录
选择格式与所有者
Compression Streams 规范将 CompressionStream 和 DecompressionStream 定义为 TransformStream 接口。构造函数接收格式。gzip 用头部和尾部包装 DEFLATE,deflate 使用 zlib 包装形式,deflate-raw 则是原始 DEFLATE 流。生产者与消费者必须约定同一格式,API 同时存在并不会自动协商。
MDN CompressionStream 和 MDN DecompressionStream 说明了格式和可用性。选择原生路径前检查两个构造函数。缺少构造函数是能力结果,不是数据损坏的证据。Chrome Developers 文章介绍了流式模型,但不保证每个 webview 或服务器适配器都相同。
连接 stream 前先分配所有权。接收 Response.body 的代码拥有 reader、AbortController、解压器和解码器,直到把完整结果交给应用。记录 fixtureId、format、source、consumer 和 outcome,不要记录私人文本或完整 URL。
不要混淆传输压缩和应用压缩。fetch 可能在交给 JavaScript 前自动解码 Content-Encoding: gzip,此时再使用 DecompressionStream('gzip') 就是重复解压。另一个二进制 endpoint 可能返回不透明的 gzip member,此时由应用负责解压。添加转换前先查看响应头和协议。
上传也有同样的边界。浏览器转换不会自动设置 Content-Encoding,而一个头部也不能证明字节是有效 gzip。把请求头、转换格式和服务器响应作为三个字段记录。BotBrowser 支持复现获授权 fixture 并记录可见状态,但不会替远程 origin 选择策略。
构造有界字节管线
使用 pipeThrough() 和 pipeTo(),让 backpressure 与取消保留在 streams 模型中:
const controller = new AbortController();
const input = new Blob([new TextEncoder().encode('owned fixture')]).stream();
const compressed = input.pipeThrough(new CompressionStream('gzip'));
const output = new WritableStream({
write(chunk) {
recordChunk(chunk);
},
close() {
record.outcome = 'compressed';
},
abort(reason) {
record.outcome = 'aborted';
record.reason = String(reason);
},
});
await compressed.pipeTo(output, { signal: controller.signal });
示例记录 chunk,而不是无界拼接数组。消费者控制需求,所有者可以取消整条管线。chunk 边界可能随浏览器版本改变,也不代表原始网络包。测量总字节和可见完成,不要假设 write() 调用次数。
设置字节预算,例如压缩数据 8 MB,以及应用对解压输出的单独上限。它可以防止过度膨胀,但不是 checksum。记录 bytesIn、bytesOut、limitBytes 和 outcome,不要保存数据。codec 接受字节不等于 JSON、归档或 schema 有效。
tee() 会产生拥有独立需求与取消的分支。不要只为记录原始字节而复制响应。worker 可以拥有 codec,但应发送 compression-complete 这样的短状态,而不是整个响应体。第一个 reader 关闭 body 后,第二个 reader 无法恢复字节。
处理文本、取消与错误
解压输出的是字节。之后再应用 TextDecoder,编码选择要来自数据契约。转换成功不能证明 UTF-8。{ fatal: true } 可以区分 decode-error 与 decompress-error,替换字符则可能掩盖损坏。
const decoded = compressedResponse.body.pipeThrough(new DecompressionStream('gzip'));
const reader = decoded.getReader();
const decoder = new TextDecoder('utf-8', { fatal: true });
let text = '';
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
record.outcome = 'decoded';
} catch (error) {
record.outcome = error.name === 'AbortError' ? 'cancelled' : 'decode-error';
} finally {
reader.releaseLock();
}
大响应应增量消费,不要使用 text +=。body 可能为 null、已经消费或被其他 reader 锁定。重试前区分 body-null、body-locked、unsupported-format、decompress-error 和 decode-error。
取消可能发生在收到字节之前、部分写入之后,或导航删除组件之后。恢复动作不同时要区分 cancelled、sink-error 和 owner-gone。中止管线不会删除已经发送的字节,也不会删除其他服务保留的副本。
解压后的数据不是真实性证明。某些格式的 checksum 只能发现 framing 错误,不是应用签名。需要真实性时单独验证签名,并把结果与解压状态并列保存。
使用 fixture 验证边界
使用四行合成 fixture:原生 gzip 压缩、原生 gzip 解压、被取消的 pipe、错误字节。分别在构造函数存在、构造函数缺失和 body 已消费时运行:
| 情况 | 预期状态 | 调查类别 |
|---|---|---|
CompressionStream('gzip') 接收字节 | compressed 且 bytesOut 有界 | unsupported-format 或 sink 错误 |
DecompressionStream('gzip') 接收 fixture | decoded 且 UTF-8 有效 | decompress-error 或 decode-error |
| 取消信号触发 | cancelled 且两个所有者关闭 | reader 泄漏或覆盖部分结果 |
| 错误 member 到达解压器 | decompress-error | 无限重试或空成功 |
Response.body 为 null 或已消费 | body-null 或 body-locked | 所有权假设错误 |
记录 browserVersion、implementation、format、bytesIn、bytesOut、limitBytes、outcome 和 fixtureId。值应合成且有界。BotBrowser 可以记录 compressed、decoded、cancelled 和 decompress-error,但不能控制远程 Content-Encoding、代理是否保留每个字节,也不能认证密码学完整性。
通过 service worker 和 window 各测试一次相同资源。如果 worker 解压并返回新的 Response,页面不应再次解压。如果 worker 传递不透明二进制 member,页面拥有转换。上传和下载虽然使用同一 API,但所有者和恢复动作不同,必须分开记录。
诚实的回退可以使用经过审核的库、服务器 endpoint 或无压缩路径。在 implementation 中标记 native、fallback-unavailable 或 server-decoded,不要承诺相同的 chunking 或 timing。浏览器、服务器、parser 或 service worker 改动后,重复相同行并比较第一个变化字段。
Public sources
- Compression Streams specification
- MDN: CompressionStream
- MDN: DecompressionStream
- Chrome Developers: Compression Streams API
- BotBrowser: Automation consistency
实践结论
声明格式,为每个 stream 分配一个所有者,并分别记录解压、解码、取消和完整性。通过消费者施加 backpressure,限制字节预算,reader 出错后直接关闭而不是重试确定性损坏。浏览器、服务器或 service worker 变化后重放有界 fixture,并依据第一个变化状态作出决定。
本文属于指南: Web 平台能力面:一致性复核地图