纸上山河 记录技术、思考与生活

Nginx 静态站缓存策略:hash 文件长缓存,HTML 不缓存

一份可以直接抄的配置

约 2 分钟 making

静态站的缓存策略只有两条规则,但顺序和写法都很容易错。

规则本身

# 带 hash 的构建产物:内容变了文件名就变,可以永久缓存
location /_astro/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML:文件名不变但内容会变,不能缓存
location / {
    expires -1;
    add_header Cache-Control "no-cache";
}

为什么这么配

关键在于两种资源的变更方式不同:

资源文件名内容变化时缓存策略
/_astro/main.abc123.js变了变了永久缓存
/index.html不变变了必须回源校验

文件名变了,旧缓存自然失效,不需要手动清理。文件名不变但内容变了,如果缓存了,访客就会一直看到旧版本——这就是「我明明发了文章,为什么没人看到」的原因。

immutable 是关键

add_header Cache-Control "public, immutable";

immutable 告诉浏览器:即使用户按了刷新,也不要发条件请求去问服务器。配合 hash 文件名,这是安全且激进的。

压缩也要配

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
    text/plain text/css text/xml
    application/javascript application/json
    application/rss+xml image/svg+xml;

gzip_min_length 1024 很必要——小文件压缩后可能更大,反而浪费。

如果服务器支持,Brotli 比 gzip 再好 15% 左右。

三个容易漏的细节

1. server_tokens off —— 不要暴露 Nginx 版本号

2. 安全响应头 —— 静态站也要加:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;

3. 404 指向自定义页

error_page 404 /404.html;
location = /404.html { internal; }

internal 防止用户直接访问这个页面被当成正常内容。

一个完整的最简配置

server {
    listen 80;
    server_name example.com;
    root /usr/share/nginx/html;

    server_tokens off;
    charset utf-8;

    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    location /_astro/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location / {
        expires -1;
        add_header Cache-Control "no-cache";
    }

    error_page 404 /404.html;
    location = /404.html { internal; }

    add_header X-Content-Type-Options "nosniff" always;
}

判断缓存配得对不对有个简单方法:发一篇新文章,等 30 秒刷新——应该能立刻看到。看不到就是 HTML 被缓存了。