Ccyriak.xyz

Nginx 静态站的缓存与压缩取舍

目录

静态站的性能配置其实只有两件事:告诉浏览器什么能存、存多久;以及决定服务端要不要压。但这两件事最常见的写法都是错的——不是配得不够,是配得太狠或者配反了。

缓存时长不能一刀切

把所有文件都设成 expires 30d 是最省事也最糟糕的做法:改了 HTML,访客要等一个月才看得到。正确的做法是按「文件名会不会变」来分。

nginx
# 1) 带内容指纹的资源:可以永久缓存
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# 2) HTML:必须回源校验
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache, must-revalidate";
}

# 3) 图片、字体:名称通常不变,给长缓存
location ~* \.(png|jpe?g|gif|webp|svg|woff2?)$ {
    expires 180d;
    add_header Cache-Control "public, max-age=15552000";
}

关键是 immutable。它告诉浏览器:这个 URL 的内容不会变,连条件请求都别发了。代价是你必须在内容变化时改文件名,比如 style.a1b2c3.css。如果文件名是固定的,就别用 immutable,否则用户会一直看到旧样式。

这类站每次更新都是手动传文件。所以我干脆不做指纹,改成:CSS/JS 走短缓存(max-age=3600),HTML 走 no-cache。多一次条件请求,换掉「改了不生效」的困惑,更划算。

add_header 的层叠陷阱

这是我自己踩过、并且很多配置里都能看到的错误:只要在某个层级写了 add_header,上一层的 add_header 就全部失效,不是叠加。

nginx
server {
    add_header X-Frame-Options "SAMEORIGIN";
    add_header Referrer-Policy "no-referrer";

    location /assets/ {
        add_header Cache-Control "public, max-age=31536000";
        # ↑ 这里一写,上面两条安全头在这个 location 里全部消失
    }
}

要么在每个 location 里把需要的头补全,要么把 add_header 全部提到 server 层、location 里只用 expires。后者更好维护。

gzip:只压文本,别贪级别

压缩级别从 5 到 9,文件通常只再小 1–2%,CPU 时间却翻倍。对这种低配机器,5 或 6 是合适的位置。

nginx
gzip              on;
gzip_vary         on;
gzip_comp_level   5;
gzip_min_length   512;
gzip_proxied      any;
gzip_types
    text/plain
    text/css
    text/xml
    application/javascript
    application/json
    application/xml
    image/svg+xml;
  • gzip_min_length 512——几十字节的响应压缩后反而更大,还会多出协商开销
  • gzip_vary on——加上 Vary: Accept-Encoding,避免 CDN 把压缩版缓存给不支持的客户端
  • 不要把 image/png、image/jpeg、video/mp4 加进 gzip_types,它们已经是压缩格式,再压一遍只浪费 CPU

一个实际数字

这个站首页大约 2.6KB 的 HTML,gzip 之后约 1.1KB。压缩率高是因为中文内容重复模式少、文本熵低;但绝对值上省了 1.5KB,对首屏的影响其实很小。

真正省时间的是减少请求数和避免阻塞渲染的外部资源。这个站没有字体文件、没有外部 CSS 框架、没有统计脚本,所以即使不压缩也不会慢。压缩是锦上添花,不是解决方案。

要不要上 Brotli

Debian 官方源里的 Nginx 不带 ngx_brotli,得自己编译或者用带模块的第三方包。Brotli 在文本上比 gzip 再小 15% 左右,但代价是多一个可能出问题的构建链路。

我的判断标准很简单:如果站点总传输量在几百 KB 以内,不值得。等你需要传几 MB 的 JS 包时,再考虑它。

验证

bash
curl -sI -H "Accept-Encoding: gzip" https://example.com/assets/style.css \
  | grep -iE "cache-control|content-encoding|vary|expires"

# 期望看到
# cache-control: public, max-age=31536000, immutable
# content-encoding: gzip
# vary: Accept-Encoding

改完配置先 nginx -t 再 systemctl reload nginx,永远不要直接 restart——reload 不会断掉已有连接。