根本原因是Nginx未找到匹配的预压缩.gz文件,或原始文件与.gz文件mtime不一致;gzip_static仅查找同名.gz文件且严格校验修改时间,若缺失或时间不同步则回退并额外执行stat调用,导致变慢。

为什么 gzip_static 不生效,反而还变慢了?
根本原因通常是 Nginx 没有找到对应预压缩文件,或配置未匹配请求路径与 MIME 类型。它不会自动压缩,只查找同名 .gz 文件(如 /js/app.js → 查找 /js/app.js.gz),且要求原始文件和压缩文件的修改时间一致(gzip_static on 严格校验 mtime)。若原始文件被更新但 .gz 未重生成,Nginx 直接跳过并回退到动态 gzip,甚至因多一次文件 stat 而略慢。
- 确保构建流程中每次更新 JS/CSS 都同步运行
gzip -k -9 <file></file>(-k保留原文件) - 检查文件权限:Nginx worker 进程必须对
.gz文件有读权限,且与原始文件属同一用户组(常见于 Docker 或 systemd 服务中权限隔离场景) - 确认
gzip_static值为on(不是always;后者已废弃且不兼容多数版本)
gzip_static on 和 gzip on 能否共存?
可以,而且推荐共存——gzip_static 处理已预压缩资源,gzip 作为兜底处理动态内容或漏掉的静态文件。但二者行为有关键差异:gzip_static 不受 gzip_types 控制,只要请求路径匹配且存在有效 .gz 文件就直接返回;而 gzip 会检查 gzip_types、客户端 Accept-Encoding、响应体大小(默认 gzip_min_length 20 字节)等条件。
- 务必关闭
gzip_vary off(默认是on),否则即使用了.gz文件,Nginx 仍会加Vary: Accept-Encoding头,影响 CDN 缓存命中 - 不要在
location块里重复开启gzip,全局启用一次即可;gzip_static则建议按静态资源 location 单独配置,避免误触 API 接口 - 若用
try_files,注意gzip_static在try_files之前生效,顺序错误会导致找不到.gz
如何验证 gzip_static 真正起作用?
不能只看响应头有没有 Content-Encoding: gzip,因为这可能是动态压缩的结果。真正证据是响应头中出现 Content-Length 与预压缩文件大小一致,且 ETag 值等于 .gz 文件的 inode + mtime 组合(而非原始文件)。
- 用
curl -I -H "Accept-Encoding: gzip" https://yoursite.com/app.js查看响应头,对比Content-Length和本地stat -c "%s" app.js.gz - 用
strace -e trace=openat,stat -p $(pgrep nginx)抓取 worker 进程系统调用,能看到它是否 open 了app.js.gz - 在日志中添加
$gzip_ratio变量(仅对动态 gzip 生效),若该值恒为 “-” 且响应正常,大概率走的是 static 分支
Docker 或 CI/CD 环境下容易忽略的构建细节
预压缩文件必须和原始文件一起打进镜像或部署包,且路径完全一致。很多前端构建工具(如 Webpack/Vite)默认不输出 .gz,需显式配置插件或 postbuild 脚本;更隐蔽的问题是构建机时区与运行时容器时区不一致,导致 mtime 校验失败。
- Vite 用户可加插件
vite-plugin-compression并设algorithm: 'gzip',同时开deleteOriginalAssets: false - Dockerfile 中避免用
COPY --chown单独改原始文件权限却不改.gz文件,应统一chmod 644 *.js *.js.gz - CI 流水线里用
touch -r app.js app.js.gz同步时间戳,比依赖构建时间更可靠
最常被绕过的点:Nginx 的 root 或 alias 指令决定了它去哪找 .gz 文件,而这个路径和你 curl 的 URL 路径之间是字符串拼接关系,没有“智能解析”。写错一个斜杠,.gz 就永远找不到。


















