先搞清 expires 和 Cache-Control 的关系
结论先行:expires 是 Nginx 的便捷指令,它最终会生成 Expires 和 Cache-Control: max-age 两个响应头;而 add_header Cache-Control 是手动直接写头。两者同时出现时,浏览器以 Cache-Control 为准,Expires 只是给老客户端的兜底。所以静态资源缓存的核心就是一条:给不同后缀的文件设置合理的 max-age。
基础配置:按文件类型分别设过期时间
在 server 块里用 location 匹配后缀,是最常用也最稳的做法:
server {
listen 80;
server_name example.com;
root /var/www/html;
# 图片、字体、媒体:内容基本不变,缓存一年
location ~* .(gif|jpg|jpeg|png|webp|svg|ico|woff2?|ttf|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
# CSS/JS:用文件名哈希做版本控制,同样缓存一年
location ~* .(css|js)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存,保证入口页及时更新
location ~* .(html|htm)$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
注意:expires -1 会生成 Cache-Control: no-cache,表示“可缓存但每次要回源校验”。如果你希望 HTML 完全不落盘,用上面 add_header 里的 no-store。
用 map 做统一版本号(推荐进阶写法)
如果不想在每个 location 里重复写时间,可以用 map 按后缀集中管理,改一处即可全站生效:
http {
map $sent_http_content_type $cache_time {
default off;
~image/ 365d;
~font/ 365d;
~text/css 365d;
~application/javascript 365d;
~text/html -1;
}
server {
listen 80;
server_name example.com;
root /var/www/html;
location / {
expires $cache_time;
}
}
}
这里用 $sent_http_content_type 匹配 MIME 类型,比裸后缀更准确,但要注意它依赖上游返回的 Content-Type,纯静态文件由 Nginx 自己根据 mime.types 判断,通常没问题。
验证与常见坑
配置改完先测试再重载:
nginx -t
nginx -s reload
用 curl 看响应头,确认 Cache-Control 和 Expires 是否符合预期:
curl -I https://example.com/static/app.css
几个容易踩的坑:
- add_header 会覆盖父级:如果在
server块写了add_header,子location里再写会替换掉父级那一组,不是叠加。建议把缓存头只放在需要的 location 里。 - 带哈希的文件名才敢用 immutable:
immutable表示浏览器在过期前绝不发校验请求。如果文件名固定(如style.css),千万别加,否则用户永远拿不到新版本。 - CDN 会再缓存一层:源站设了
max-age=31536000,CDN 节点也照此缓存。更新文件时要么改文件名,要么去 CDN 控制台刷新,光 reload Nginx 没用。 - expires 与 add_header 同时写 Cache-Control 会冲突:Nginx 可能输出两个
Cache-Control头,浏览器行为不可预测。二选一,或者用expires生成基础值、不再手动加Cache-Control。
最后给个实用建议:CSS/JS 走构建工具生成带哈希的文件名,配 expires 365d; add_header Cache-Control "public, immutable";;HTML 走 no-cache 或很短的 max-age。这样既最大化利用缓存,又能让每次发版立即生效。


