题干与适用场景
浏览器需要压缩上传的大文件并解压下载的 gzip 数据,要求低内存、可取消且不因恶意输入失控。请设计流式方案并说明格式、背压、错误和安全边界。
Compression Streams API 提供 CompressionStream 与 DecompressionStream,把二进制块接入 Web Streams 管线。规范定义了 brotli、deflate、deflate-raw 和 gzip 格式;API 提供转换流,但不会替应用设置大小上限、鉴权或业务完整性校验。
面试官考察点
重点包括:Readable/Writable/TransformStream 的连接、背压和队列、结束 flush、压缩格式边界、解压错误、取消传播、内存上限、解压炸弹与长度侧信道,以及渐进增强和服务端协商。
30 秒回答框架
“我会用 Blob.stream() 或响应体接入 CompressionStream,通过 pipeThrough 保持背压,使用 AbortSignal 传播取消。下载端先限制压缩包大小、解压字节数和处理时间,再把 DecompressionStream 接入解析器;格式和校验失败立即终止并清理资源。浏览器能力不足时回退到服务端压缩,不能把 API 当作安全扫描器或完整性校验器。”
分步骤深入解答
第一步:选择输入输出流
上传可从 Blob.stream() 得到字节流,下载可使用 Response.body。中间通过 CompressionStream 产生 ReadableStream,最终交给 fetch 请求体、文件写入或解析器;避免先把整个文件读入 ArrayBuffer。
第二步:连接转换流
最小上传管线如下:
async function uploadGzip(blob, signal) {
const compressed = blob.stream().pipeThrough(
new CompressionStream("gzip"),
{ signal },
);
return fetch("/upload", {
method: "POST",
body: compressed,
signal,
headers: { "Content-Encoding": "gzip" },
});
}真实服务还要约定请求体长度、重试语义和服务端是否接受流式请求。
第三步:理解背压和 flush
Streams API 会根据下游队列和写入速度调节上游读取。不要在 data 回调中无限累积数组;下游变慢时应让管线自然暂停。关闭写端时转换流必须执行 flush,否则最后的压缩块或校验信息可能没有输出。
第四步:处理格式与协商
CompressionStream 构造函数接收受支持的格式字符串;不支持的格式会抛出错误。客户端应根据服务端协议明确 Content-Encoding 或业务字段,不能把 deflate、deflate-raw 和 gzip 当作同一种封装,也不能未协商就发送 Brotli。
第五步:设计安全解压
压缩数据可能以很小输入展开成巨大输出。解压端要统计输出字节数、文件条目、处理时间和并发任务,超过预算就调用 cancel 或 abort。解压后的内容仍需 MIME、路径和内容安全检查,不能因为来自 DecompressionStream 就信任。
第六步:传播错误与取消
压缩或解压校验失败会使转换流进入错误状态。用 try/catch 处理 pipeTo、fetch 和 reader 错误,并把用户取消传播到请求、读写端和转换流;清理临时 Blob、锁和 UI 进度状态,避免悬挂读取器。
第七步:处理隐私与完整性
压缩长度可能泄露秘密与攻击者可控文本的关系。不要把机密和用户可控内容放入同一压缩上下文;对重要文件使用独立的签名或哈希校验,压缩只负责编码,不负责来源认证或防篡改。
第八步:制定兼容与降级
启动时检测构造函数和目标格式,在 Worker 中执行大文件任务以隔离主线程。能力不足时交给服务端压缩或直接上传,并保持相同的取消、大小限制和错误语义;不要只在现代浏览器上验证。
设计取舍与边界
客户端 CPU 还是网络节省
压缩可降低上传字节数,但会消耗 CPU、电量和时间。移动端应依据文件类型、网络质量和电量做策略,已压缩格式通常不应再次压缩。
流式处理还是重试简单
流式管线低内存,却要求服务端支持分块和幂等重试。需要断点续传时,应把压缩块与上传分片协议绑定,不能简单重试一个已消费的 ReadableStream。
浏览器解压还是服务端解压
浏览器解压减少服务端 CPU,但把资源和安全预算放到用户设备。含敏感或高膨胀比数据时可由服务端执行并返回受控结果,前端只消费经过限制的流。
失败演练与演进计划
下游变慢导致内存增长
人为降低上传速度,观察队列和内存曲线,确认没有把所有 chunk 放入数组;若队列仍增长,降低并发并让管线暂停。
截断 gzip 输入
截断压缩流,确认解压在 flush 或校验阶段失败,UI 显示可重试状态,并释放 reader、请求和临时对象。
解压输出超预算
使用高膨胀比样本,验证输出计数达到上限后立即取消,不把完整结果写入内存或持久化存储。
常见误区与追问
误区一:流式 API 自动防止解压炸弹
追问:还缺什么?必须应用输出字节、时间、文件条目和并发预算,API 本身只执行格式转换。
误区二:deflate 等于 gzip
追问:为什么不能混用?deflate、deflate-raw 和 gzip 的封装不同,服务端必须按协议协商并使用匹配格式。
误区三:直接重试已消费的流
追问:正确做法是什么?从可重放的 Blob 或分片源重新建立管线,并用幂等上传标识协调服务端。
延伸追问与参考答案
为什么要关注 flush?
转换流在输入结束时需要输出尾部压缩数据和校验信息;没有正常关闭写端,消费者可能收到不完整流。
如何降低长度侧信道风险?
隔离机密与攻击者可控文本,避免共享压缩上下文,并在协议层加入固定填充或改用不暴露长度关系的设计。
哪些情况应优先服务端压缩?
旧浏览器、低电量设备、超大文件或敏感数据场景可由服务端承担压缩与解压,但仍要限制输出并向前端返回可观测状态。