compressionstream 是现代浏览器原生支持的流式压缩 api,适用于 dom 快照实时压缩上传,具备零拷贝、异步流式、自动分片和无 polyfill 依赖等优势。

CompressionStream 是现代浏览器(Chrome 109+、Firefox 113+、Safari 17.4+)原生支持的可读/可写流压缩 API,专为 Web 环境设计,无需引入第三方库或手动管理 Worker,天然适配 fetch、ReadableStream 和 TransformStream。它特别适合将大型 DOM 状态快照(如序列化后的完整页面结构、组件树、表单数据、Canvas 像素缓冲等)在发送前实时压缩,显著降低上传带宽与延迟。
✅ 为什么用 CompressionStream 而不是 Blob + gzip 工具?
-
零内存拷贝:直接管道式处理,避免将整个快照转成
ArrayBuffer或Blob再压缩(易触发 OOM); - 真正异步流式:边序列化、边压缩、边上传,三阶段重叠,不阻塞主线程;
-
自动 chunk 分片:配合
fetch()的body: ReadableStream,浏览器底层按需分块推送,服务端可边收边解压; - 无 polyfill 依赖:纯标准 API,兼容性明确,未来维护成本低。
? 步骤一:生成轻量但完整的 DOM 快照流
不要用 document.documentElement.outerHTML(体积大、含冗余样式/脚本)。推荐:
- 使用
document.cloneNode(true)+ 清理策略(移除script、style、data-*等非状态属性); - 或更优:用
serializeDOM()类函数只提取关键状态(input.value、textarea.value、checked、selectedIndex、dataset.state等),输出为精简 JSON 流;
async function* domSnapshotStream() {
const state = {
url: location.href,
timestamp: Date.now(),
inputs: Array.from(document.querySelectorAll('input, textarea, select'))
.reduce((acc, el) => {
acc[el.name || el.id] = el.type === 'checkbox' ? el.checked : el.value;
return acc;
}, {})
};
yield new TextEncoder().encode(JSON.stringify(state));
}
✅ 输出是
AsyncIterable<uint8array></uint8array>,可直接接入CompressionStream。
? 步骤二:构建压缩传输流水线
async function uploadCompressedSnapshot() {
const snapshotStream = domSnapshotStream();
const compressedStream = new CompressionStream('gzip');
// 管道:snapshot → compress → upload
const readable = (async function* () {
for await (const chunk of snapshotStream) {
yield chunk;
}
})();
const body = readable
.pipeThrough(compressedStream)
.pipeThrough(new TextEncoderStream()); // 可选:确保最终为 Uint8Array 流
const res = await fetch('/api/snapshot', {
method: 'POST',
headers: {
'Content-Encoding': 'gzip',
'Content-Type': 'application/json'
},
body
});
return res.ok;
}
⚠️ 注意:
-
Content-Encoding: gzip必须显式设置,否则服务端无法识别压缩格式; -
body是ReadableStream<uint8array></uint8array>,fetch自动处理流式上传; - 不需要
await整个流结束才发请求 —— 流一旦启动,上传立即开始。
? 步骤三:服务端配合(以 Node.js Express 为例)
const zlib = require('zlib');
app.post('/api/snapshot', express.raw({
type: '*/*',
limit: '50mb'
}), (req, res) => {
const encoding = req.get('content-encoding')?.toLowerCase();
let stream = req;
if (encoding === 'gzip') {
stream = req.pipe(zlib.createGunzip());
}
let data = '';
stream.on('data', chunk => data += chunk);
stream.on('end', () => {
try {
const snapshot = JSON.parse(data);
// 存储、分析、回放...
res.status(200).send('ok');
} catch (e) {
res.status(400).send('invalid json');
}
});
});
✅ 关键点:
- 用
express.raw()接收原始字节流(不能用json()或text()中间件); - 根据
Content-Encoding动态解压,保持流式处理; - 全程不缓存整包,内存占用恒定(~几 KB buffer)。
⚙️ 进阶建议(提升可靠性与可观测性)
- 添加简单校验:在快照 JSON 外层包裹
{ version: 2, checksum: 'xx', data: { ... } },压缩前计算sha256片段; - 超时控制:用
AbortController包裹fetch,防止卡死长流; - 错误回退:检测
CompressionStream不可用时(旧浏览器),降级为JSON.stringify()+Blob+FormData; - 监控压缩率:通过
compressedStream.readable.locked和writable.closed配合计时器,统计originalSize / compressedSize。
不需要打包、不依赖 WebAssembly、不新建 Worker —— 利用浏览器原生流能力,就能安全、高效、渐进式地上传海量 DOM 快照。











