← 中文资源

简体中文版本

安全地为响应启用压缩

用 gzip 压缩 JSON 与 HTML,跳过已压缩资源,正确设置 Vary,并实测传输体积收益而不是凭感觉假设。

阅读英文原文

概述

压缩是内容协商的结果,而不是无条件行为。客户端发送 Accept-Encoding: gzip, br,服务端才可以选择编码。若服务端强行压缩,或压缩了不该压缩的类型,不仅浪费字节,还会让共享缓存产生歧义。中间件在写出响应时运行,能看到最终的 Content-Type,因此每个响应只判断一次,实现简单且可靠。

步骤

  1. 安装并在路由之前注册压缩中间件,设置 1 KB 阈值:
import compression from 'compression';
app.use(compression({ threshold: 1024 }));
  1. 在过滤器中排除图片、视频、压缩包与字体等已压缩类型:
app.use(compression({
  threshold: 1024,
  filter: (req, res) => {
    const type = String(res.getHeader('Content-Type') ?? '');
    if (/^(image|video|audio)\//.test(type)) return false;
    if (/(zip|gzip|br|woff2)$/.test(req.path)) return false;
    return compression.filter(req, res);
  },
}));
  1. 为所有可压缩响应保留 Vary: Accept-Encoding,让共享缓存按编码分别存储变体。
  2. 用 curl -H 'Accept-Encoding: gzip' 与 identity 各取一次,对比 size_download。

验证

同一份 JSON 在 gzip 下应从约 400 KB 降到 18 KB 左右;不带 Accept-Encoding 的请求必须返回原始字节且没有 Content-Encoding。同时确认 304 Not Modified 没有响应体、没有被压缩,SSE 接口仍然逐条增量推送。

注意事项

  • 代理与应用同时压缩会导致双重编码,必须只保留一层负责 Content-Encoding。
  • 压缩消耗 CPU,小响应与已压缩类型应跳过;大静态资源建议构建期预压缩交给 CDN。
  • 混合了密钥与攻击者可控内容的响应可能遭受 BREACH 类攻击,这类接口应关闭压缩。
  • 除了体积,还要观察 p99 延迟与 CPU 占用,避免用延迟换体积。