对个人站长来说,网站提速性价比最高的手段,不是换服务器、不是上 CDN,而是把 HTTP 缓存配置好。很多站第一次访问慢,第二次访问还是慢,就是因为完全没有利用浏览器缓存——同样的图片、CSS、JS,访客每次打开页面都要重新下载一遍。这篇文章把 HTTP 缓存的两种类型、响应头参数、以及 Nginx 下的具体配置一次讲清楚,配完立竿见影。
一、强缓存与协商缓存
HTTP 缓存分为两大类。强缓存也叫本地缓存,指的是浏览器发现资源没过期,直接使用本地副本,连请求都不发,响应状态是 200 (from disk cache) 或 200 (from memory cache)。协商缓存则是浏览器带着资源的标识去问服务器这个资源变了吗,服务器说没变,返回 304 Not Modified,浏览器继续用本地副本,但这次请求还是发出去了,省下的是传输体积。强缓存靠 Cache-Control 和 Expires 控制,协商缓存靠 ETag 和 Last-Modified 控制。理解这两者的区别是配置缓存的第一步:能强缓存就强缓存,协商缓存是兜底。
二、Cache-Control 详解
Cache-Control 是 HTTP/1.1 的缓存控制头,字段非常多,站长只需要掌握几个核心值。max-age=31536000 表示资源在 31536000 秒,也就是一年内可以直接用本地缓存。no-cache 不是不缓存,而是缓存但每次都要去服务器确认,相当于强制走协商缓存。no-store 才是真正的完全不缓存,适合存放隐私数据的接口。public 表示任何节点包括 CDN 都可以缓存,private 表示只有浏览器可以缓存,中间节点不能缓存。还有一个进阶用法 immutable,配合 max-age 使用,告诉浏览器这个资源永远不会变,连重新验证都省了,适合带版本号的静态资源。实际配置中,HTML 页面用 no-cache 或很短的 max-age,静态资源用一年甚至更长的 max-age,是最常见的组合。
还有一个容易被忽略的头是 Vary。当资源存在多个版本,比如 gzip 压缩版和未压缩版时,Vary: Accept-Encoding 告诉缓存节点:同一个 URL 的内容可能因请求头不同而不同,必须区分存储。如果你开启了 gzip 却忘了处理 Vary,就可能出现 CDN 给不支持压缩的老客户端返回了压缩内容、导致乱码的经典事故。Nginx 在 gzip 开启时默认会带上 Vary: Accept-Encoding,但如果你用了第三方模块或者自定义了头,记得确认这一点。
三、ETag 与 Last-Modified
协商缓存有两个标识。Last-Modified 是资源的最后修改时间,由服务器自动生成,优点是零成本,缺点是精度只有秒级,而且有些场景下内容变了但修改时间没变。ETag 是资源的指纹,本质上是内容的哈希,内容一变 ETag 就变,更精确。Nginx 默认同时开启这两个,浏览器请求时带上 If-Modified-Since 和 If-None-Match,服务器比较后返回 304。注意一个坑:如果你的网站挂在 Nginx 后面,而 Nginx 对 ETag 的处理在某些版本里有缺陷,比如 gzip 压缩后 ETag 计算不一致,会出现明明内容没变却返回 200 全量下载的情况。遇到这种问题,可以在配置里加 etag off 或者调整 gzip 配置来规避。
四、Nginx expires 配置实战
Nginx 里配置缓存非常简单,核心就是 expires 指令。给静态资源设置一年缓存:
location ~* \.(css|js|png|jpg|jpeg|gif|ico|webp|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}对于 favicon 和 robots.txt 这种几乎不变的文件,也可以直接套用。关键原则是:文件名带版本号,比如 style.20240101.css,或者内容哈希的资源,可以放心用一年以上的强缓存;而不带版本号的资源,max-age 不要设太长,否则用户更新了文件也看不到变化。这也是为什么很多前端构建工具会把文件名改成带 hash 的形式,目的就是为了配合长缓存。
配置的时候还要注意 location 匹配的优先级。上面用的是正则匹配,优先级高于普通前缀匹配,但低于精确匹配。如果你的站点把静态文件放在 /static/ 目录下,也可以写成 location /static/ { expires 1y; ... } 这种前缀匹配的形式,效果一样。另外 add_header 指令有个坑:它在当前 location 没有其他 add_header 的时候才生效,如果你在 server 层已经加过 add_header,location 里再加会把 server 层的覆盖掉,配置前最好先梳理清楚,避免调试半天发现头根本没加上。
五、动态页面的缓存策略
HTML 页面,包括 Typecho 这类博客系统的文章页,是动态生成的,不能无脑长缓存,否则评论、新文章更新后用户看不到。比较稳妥的做法是:HTML 页面用 no-cache,让浏览器每次都向服务器确认,服务器返回 304 就省下传输;或者用很短时间的强缓存,比如 expires 5m,五分钟内的重复访问直接命中本地缓存。另外可以针对不需要实时更新的页面,比如关于页,单独加长缓存。如果网站访问量比较大,更推荐在 Nginx 层面做整页缓存,比如用 fastcgi_cache 缓存 PHP 输出,这个方案对个人站来说收益极高,很多博客站开启后页面响应时间能从几百毫秒降到个位数毫秒,但配置相对复杂,涉及缓存 key 设计、缓存清理等问题,适合有一定经验后再上。
这里给一个 fastcgi_cache 的最小配置作为参考,帮你建立直观印象:fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=phpcache:100m inactive=60m; 定义缓存目录和共享内存区,然后在 server 里 fastcgi_cache phpcache; fastcgi_cache_key $scheme$request_method$host$request_uri; 定义缓存键,最后用 fastcgi_cache_valid 200 301 10m; 指定哪些状态码缓存多久。开启后记得测试评论、登录等动态交互功能是否正常,因为这类请求如果被缓存,会导致用户数据不同步,需要按实际情况调整缓存键或者跳过带 cookie 的请求。
六、用 curl 验证缓存是否生效
配置完之后必须验证,别配完就以为万事大吉。用 curl 查看响应头是最直接的方法:
curl -sI https://www.example.com/style.css看返回的头里有没有 Cache-Control: max-age=31536000、Expires、ETag、Last-Modified。第一次请求响应是 200 是正常的,第二次请求带上 -H 'If-None-Match: 对应的ETag' 再发,如果返回 304,说明协商缓存也生效了。还可以用 Chrome 开发者工具里的 Network 面板,勾选 Disable cache 对比开启和关闭时的加载时间,差异会非常直观。我自己的站点配置完这套缓存后,静态资源全部命中强缓存,二次访问的加载时间大约减少了一半,效果立竿见影。
如果发现响应头里根本没有缓存相关的头,先检查是不是走了 CDN 且 CDN 覆盖了源站的响应头,很多 CDN 默认会改写部分头部,需要到 CDN 控制台调整。另外注意 curl -I 发的是 HEAD 请求,个别服务器对 HEAD 的处理和 GET 不完全一致,如果 HEAD 看不到头,可以用 curl -sD - -o /dev/null 换成 GET 再验证一次,避免误判。
七、缓存更新踩坑实录
缓存配置好之后,最大的麻烦是更新。最常见的情况是:你改了 CSS,但用户的浏览器里还是旧样式。这就是强缓存的副作用。解决办法有三种。第一种,文件改名:新文件叫 style.20240201.css,页面引用也更新,旧文件等缓存过期自然淘汰,这是最推荐的方式。第二种,加版本参数:style.css?v=20240201,注意有些 CDN 和浏览器对带查询参数的缓存策略不一致,效果不如改名可靠。第三种,主动清缓存:如果你是 CDN 用户,可以在控制台刷新 URL 缓存;如果是纯 Nginx,只能等 max-age 过期。所以一定要记住:给静态资源配置长缓存之前,先想好更新策略,否则上线新版本时会被缓存坑得很惨。
对于自己用 Nginx fastcgi_cache 做整页缓存的站点,清理缓存也是一个必须提前想好的问题。最简单粗暴的办法是删缓存目录,rm -rf /var/cache/nginx/* 然后重启 Nginx,代价是全部页面需要重新生成,瞬间流量会打满后端;精细一点的做法是给文章页的缓存键里带上文章 ID,更新文章时单独删除对应的缓存文件。无论哪种方案,都建议把清理脚本写好并测试过,最好再配一个后台管理入口,否则每次更新文章都要 SSH 上服务器敲命令,时间长了很容易漏掉,用户就会看到旧内容。
HTTP 缓存是投入产出比最高的性能优化手段,不花一分钱,效果立竿见影。把本文的配置抄到自己的服务器上,再用 curl 验证一遍,你的网站加载速度会有肉眼可见的提升。下一步再考虑 gzip 压缩、HTTP/2、CDN 这些进阶手段,一步步来,网站性能就是这样一点一点磨出来的。