2026-10-24

静态站提速:Gzip 与缓存头的实测记录

站点能打开之后,很容易停在「反正它是静态页,应该挺快」的错觉里。这次把响应头抓下来看,才发现 /etc/nginx/nginx.conf 里关于压缩只剩孤零零一行 gzip on;,下面那一整块参数全部是注释状态,缓存头则一条都没有。这篇记录从测量到改配置的完整过程,以及中途踩到的一个小坑。

一、先量一遍现状,别猜

判断有没有压缩,看响应头最直接,不用装任何工具:

curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://eeuse.com/

返回里只有一行 content-encoding: gzip,vary 没有,cache-control 也没有。这说明 HTML 确实被压了,但除此之外什么都没有。

二、gzip on 只压了 HTML

Ubuntu 自带的 nginx.conf 里,压缩那一段长这样:

gzip on;

# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json ...;

只有一个开关打开,其余全是注释。默认行为是:只压缩 text/html,压缩级别 1,最小长度 20 字节。也就是说 CSS、JS、JSON、SVG 这些都没走压缩,HTML 用的还是最低档。对一个纯静态站来说,这等于白留了大部分收益。

三、把参数补齐

改动只在这一个文件里,替换掉原来那一行 gzip on; 所在的段落:

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript
           text/xml application/xml application/xml+rss text/javascript
           image/svg+xml application/rss+xml;
  • gzip_vary on:告诉 CDN 和代理「同一个 URL 会因编码不同而有不同响应」,漏掉它容易出现代理把压缩版发给不支持的客户端
  • gzip_comp_level 6:默认的 1 几乎不省空间,6 是体积和 CPU 的常见平衡点,静态小站完全承受得住
  • gzip_min_length 1024:太小的文件压完可能更大,还多一次 CPU,挡掉
  • gzip_types:默认只有 HTML,这一行才是真正的收益来源

四、压缩效果:实测数字

改完重载,同一份文件分别按「接受 gzip」和「不接受」各取一次长度:

GZ=$(curl -s -H "Accept-Encoding: gzip" https://eeuse.com/ | wc -c)
PL=$(curl -s -H "Accept-Encoding: identity" https://eeuse.com/ | wc -c)
  • 首页:12,072 → 4,101 字节,省掉约 66%
  • 文章页:14,542 → 6,119 字节,省掉约 58%

两页都是样式内联的单文件 HTML,压缩率差异主要来自重复的 CSS 声明。注意别被自己骗了:curl 不加 --compressed 也不会自动解压,量之前先把两种编码区分清楚,否则量到的就是压缩后的体积,白高兴一场。

先确认站点长什么样。这个站没有独立的 .css 和 .js 文件,样式全部内联在 HTML 的 <style> 里。所以下面谈缓存时,真正生效的对象是 HTML 本身,而不是想象中的那堆静态资源。

五、缓存头要按资源类型分开

缓存时间不是越长越好。HTML 是入口,发布新文章后必须尽快生效;图片、字体这类内容改一次就不再变,才适合长缓存。所以在 443 的 server 块里按类型分了两个 location:

location ~* \.(css|js|svg|png|jpg|jpeg|webp|ico|woff2?)$ {
    add_header Cache-Control "public, max-age=2592000, immutable";
    access_log off;
}

location ~* \.html$ {
    add_header Cache-Control "public, max-age=3600";
}

HTML 只给 1 小时:比「不缓存」省掉重复请求,又不至于发了文章别人两天看不到新版。真正的静态资源给 30 天并加 immutable,浏览器在有效期内连协商请求都不发。

六、踩到的坑:两条 cache-control

第一次写的时候,为了兼容只认 Expires 的老客户端,我把两种写法都留上了:

expires 1h;
add_header Cache-Control "public, max-age=3600";

结果 curl -D - 打出来是这样的:

cache-control: max-age=3600
cache-control: public, max-age=3600

两条同时出现。expires 指令本身就会自动生成一条 Cache-Control: max-age,再手写一条就重复了。浏览器大多能容忍,但这是个没必要的歧义,而且日志和调试时看着很乱。最终做法是只留 add_header 一种写法,现在响应里干净地只剩一行。

七、线上改配置的正确姿势

改配置最怕的是改错了又 reload,站点直接 502。所以我把它做成一个带自动回滚的脚本,逻辑就四步:

  1. 先备份,文件名带时间戳,nginx.conf.bak.20261024110657 这种,回滚时不用回忆
  2. 改完先 nginx -t,只做语法和文件存在性检查,不会影响正在跑的进程
  3. 检查通过才 systemctl reload nginx,不重启、不断连接
  4. 检查不通过就把备份文件拷回去,再跑一次 nginx -t 确认恢复干净
if nginx -t 2>&1; then
    systemctl reload nginx
else
    cp -a "/etc/nginx/nginx.conf.bak.$TS" /etc/nginx/nginx.conf
    nginx -t 2>&1 || true
    exit 1
fi

改完立刻验证两件事:目标页面还是 200,随手指一个不存在的路径还应该返回 404。少验一个,就可能把 try_files 改坏而不自知。

八、还能再往前一步

  • Brotli:比 gzip 再省 15% 左右,但 Ubuntu 官方 Nginx 包没带这个模块,要自己编译或换源,属于收益明显但动手成本高的一步
  • HTTP/2:这个站点早就开了,压缩和缓存是叠加收益,不冲突
  • 看真实节省:日志里同时记 $body_bytes_sent 和 $request,跑几天就能算出实际少发了多少流量

小结

  • gzip on; 只压 HTML,级别还是 1,等于只开了一小半
  • gzip_vary 和 gzip_types 是最该先补的两项
  • 压缩前先量:首页 12,072 → 4,101,文章页 14,542 → 6,119
  • 缓存要按类型分:HTML 1 小时,静态资源 30 天加 immutable
  • expires 和 add_header Cache-Control 别同时写,会出两条头
  • 改线上配置固定四步:备份、nginx -t、reload、失败回滚

本篇阅读 — 次

← 返回首页