CompressionStream/DecompressionStream 怎样在浏览器里做 gzip?
CompressionStream 和 DecompressionStream 是浏览器内置的压缩转换流,用 pipeThrough 接在 ReadableStream 后面就能做 gzip、deflate 压缩和解压,不必再打包压缩库。适合上传前压缩日志、解压 .gz 文件;它和 HTTP 的 Content-Encoding 是两回事。
简答
浏览器自带了两个转换流:
CompressionStream负责压缩,DecompressionStream负责解压。写法是stream.pipeThrough(new CompressionStream('gzip')),数据一块一块流过去,出来的就是 gzip 字节。以前要在前端压缩,得打包 pako 这类库。内置以后不用额外下载代码,也不必把整个文件读进内存再压,按流处理就行。它从 2023 年 5 月起在主流浏览器都能用,Web Worker 里也能用。
常见用法是上传前压缩大段日志或 JSON,或者在前端解压用户选的
.gz文件。拿到结果时可以把流交给new Response(stream),再调.blob()、.arrayBuffer()或.text()。要注意两点。
gzip、deflate、deflate-raw是早就支持的格式,brotli和zstd是后加的,不支持的格式在构造时直接抛TypeError,需要兜底。另外它只是处理字节,和 HTTP 响应头里的Content-Encoding没关系,浏览器对 gzip 响应本来就会自动解压。
怎样压缩一段数据再解压回来
下面把一段 6000 字节的重复文本压缩成 gzip,看看压缩后的大小和文件头,再解压回去和原文比较。Node.js 22 也提供同名的全局类,这段代码用 Node.js 22.23 实际运行过。
const text = 'hello '.repeat(1000);
const input = new Blob([text]);
const gzipStream = input.stream().pipeThrough(new CompressionStream('gzip'));
new Response(gzipStream)
.arrayBuffer()
.then((buffer) => {
const bytes = new Uint8Array(buffer);
console.log(input.size); // 6000
console.log(bytes.length < 100 ? '压缩后不到 100 字节' : '压缩效果不明显'); // 压缩后不到 100 字节
console.log(bytes[0].toString(16) + ' ' + bytes[1].toString(16)); // 1f 8b
const restored = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
return new Response(restored).text();
})
.then((result) => {
console.log(result === text ? '解压后与原文一致' : '内容不一致'); // 解压后与原文一致
});
为什么要绕一圈 new Response()?因为 pipeThrough 返回的仍然是一个 ReadableStream,它只能一块一块地读。Response 能把整个流读完并转换成 ArrayBuffer、Blob 或文本,这是 MDN 示例里推荐的收尾方式。
1f 8b 是 gzip 格式固定的开头两个字节,看到它就说明输出确实是标准 gzip,可以交给服务端的 gzip 工具解压。重复文本压缩率很高,所以 6000 字节压到了不到 100 字节,真实数据的压缩率要看内容。
格式怎么选,不支持时怎么办
构造函数的参数决定格式。gzip 和 deflate 都带校验信息,区别在外层包装:gzip 是常见的 .gz 文件格式,deflate 是 ZLIB 格式。deflate-raw 只有压缩数据本身,没有头部和尾部的校验和,适合对接要求原始 DEFLATE 数据的格式。
MDN 目前还列出了 brotli 和 zstd。整个 API 虽然标为广泛可用,但页面注明部分功能的支持程度不一样,所以用新格式前要先试:
let stream;
try {
stream = new CompressionStream('zstd');
} catch (error) {
if (!(error instanceof TypeError)) throw error;
stream = new CompressionStream('gzip');
}
MDN 写明,传入不支持的格式会抛 TypeError,所以捕获它再退回 gzip 就行。
和 HTTP 压缩有什么区别
很多人以为用了 CompressionStream 才能处理 gzip 响应,其实不用。服务端返回 Content-Encoding: gzip 时,浏览器在网络层就自动解压了,fetch 拿到的已经是原文。这部分属于内容协商,见《HTTP 内容协商会怎样影响前端资源和接口响应?》。
CompressionStream 处理的是页面自己手里的字节:要上传的日志、本地选的文件、存进 IndexedDB 的数据。比如上传前压缩,就是把 file.stream().pipeThrough(new CompressionStream('gzip')) 的结果作为请求体,同时和后端约定好它收到的是 gzip 数据。
流本身的读取、背压和取消规则与其他 ReadableStream 一样,见《怎样正确消费 Fetch 返回的 ReadableStream?》。
常见追问
压缩一定能让数据变小吗?
不一定。图片、视频、已经压缩过的 zip 本身没多少重复内容,再压一遍几乎不变,还白白花 CPU。适合压缩的是文本、JSON、日志这类重复度高的数据。
能在 Web Worker 里用吗?
能。MDN 标明这个 API 在 Web Worker 中可用。数据量大的时候放到 Worker 里压缩,可以避免占住主线程。
DecompressionStream 遇到损坏的数据会怎样?
数据不是合法的压缩格式时,流会进入错误状态,读取它的 Promise(比如
response.text())会被拒绝,所以解压用户文件时要加 catch。