代表性面试主题

前端面试:如何安全使用 Compression Streams API 处理大文件?

前端困难
Offer.cc 编辑团队发布 更新

题干

浏览器需要压缩上传的大文件并解压下载的 gzip 数据,要求低内存、可取消且不因恶意输入失控。请设计流式方案并说明格式、背压、错误和安全边界。

题干与适用场景

浏览器需要压缩上传的大文件并解压下载的 gzip 数据,要求低内存、可取消且不因恶意输入失控。请设计流式方案并说明格式、背压、错误和安全边界。

Compression Streams API 提供 CompressionStreamDecompressionStream,把二进制块接入 Web Streams 管线。规范定义了 brotlideflatedeflate-rawgzip 格式;API 提供转换流,但不会替应用设置大小上限、鉴权或业务完整性校验。

面试官考察点

重点包括:Readable/Writable/TransformStream 的连接、背压和队列、结束 flush、压缩格式边界、解压错误、取消传播、内存上限、解压炸弹与长度侧信道,以及渐进增强和服务端协商。

30 秒回答框架

“我会用 Blob.stream() 或响应体接入 CompressionStream,通过 pipeThrough 保持背压,使用 AbortSignal 传播取消。下载端先限制压缩包大小、解压字节数和处理时间,再把 DecompressionStream 接入解析器;格式和校验失败立即终止并清理资源。浏览器能力不足时回退到服务端压缩,不能把 API 当作安全扫描器或完整性校验器。”

分步骤深入解答

第一步:选择输入输出流

上传可从 Blob.stream() 得到字节流,下载可使用 Response.body。中间通过 CompressionStream 产生 ReadableStream,最终交给 fetch 请求体、文件写入或解析器;避免先把整个文件读入 ArrayBuffer

第二步:连接转换流

最小上传管线如下:

javascript
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 或业务字段,不能把 deflatedeflate-rawgzip 当作同一种封装,也不能未协商就发送 Brotli。

第五步:设计安全解压

压缩数据可能以很小输入展开成巨大输出。解压端要统计输出字节数、文件条目、处理时间和并发任务,超过预算就调用 cancelabort。解压后的内容仍需 MIME、路径和内容安全检查,不能因为来自 DecompressionStream 就信任。

第六步:传播错误与取消

压缩或解压校验失败会使转换流进入错误状态。用 try/catch 处理 pipeTofetch 和 reader 错误,并把用户取消传播到请求、读写端和转换流;清理临时 Blob、锁和 UI 进度状态,避免悬挂读取器。

第七步:处理隐私与完整性

压缩长度可能泄露秘密与攻击者可控文本的关系。不要把机密和用户可控内容放入同一压缩上下文;对重要文件使用独立的签名或哈希校验,压缩只负责编码,不负责来源认证或防篡改。

第八步:制定兼容与降级

启动时检测构造函数和目标格式,在 Worker 中执行大文件任务以隔离主线程。能力不足时交给服务端压缩或直接上传,并保持相同的取消、大小限制和错误语义;不要只在现代浏览器上验证。

设计取舍与边界

客户端 CPU 还是网络节省

压缩可降低上传字节数,但会消耗 CPU、电量和时间。移动端应依据文件类型、网络质量和电量做策略,已压缩格式通常不应再次压缩。

流式处理还是重试简单

流式管线低内存,却要求服务端支持分块和幂等重试。需要断点续传时,应把压缩块与上传分片协议绑定,不能简单重试一个已消费的 ReadableStream。

浏览器解压还是服务端解压

浏览器解压减少服务端 CPU,但把资源和安全预算放到用户设备。含敏感或高膨胀比数据时可由服务端执行并返回受控结果,前端只消费经过限制的流。

失败演练与演进计划

下游变慢导致内存增长

人为降低上传速度,观察队列和内存曲线,确认没有把所有 chunk 放入数组;若队列仍增长,降低并发并让管线暂停。

截断 gzip 输入

截断压缩流,确认解压在 flush 或校验阶段失败,UI 显示可重试状态,并释放 reader、请求和临时对象。

解压输出超预算

使用高膨胀比样本,验证输出计数达到上限后立即取消,不把完整结果写入内存或持久化存储。

常见误区与追问

误区一:流式 API 自动防止解压炸弹

追问:还缺什么?必须应用输出字节、时间、文件条目和并发预算,API 本身只执行格式转换。

误区二:deflate 等于 gzip

追问:为什么不能混用?deflatedeflate-rawgzip 的封装不同,服务端必须按协议协商并使用匹配格式。

误区三:直接重试已消费的流

追问:正确做法是什么?从可重放的 Blob 或分片源重新建立管线,并用幂等上传标识协调服务端。

延伸追问与参考答案

为什么要关注 flush?

转换流在输入结束时需要输出尾部压缩数据和校验信息;没有正常关闭写端,消费者可能收到不完整流。

如何降低长度侧信道风险?

隔离机密与攻击者可控文本,避免共享压缩上下文,并在协议层加入固定填充或改用不暴露长度关系的设计。

哪些情况应优先服务端压缩?

旧浏览器、低电量设备、超大文件或敏感数据场景可由服务端承担压缩与解压,但仍要限制输出并向前端返回可观测状态。

公开来源

同类题目