3、HTTP缓存策略:强缓存与协商缓存、Cache-Control与Expires、ETag与Last-Modified、缓存失效与更新策略
聊到服务器加速,HTTP缓存绝对是绕不开的核心。我见过太多团队,业务逻辑写得飞起,结果每次请求都回源站拉数据,白白浪费带宽和响应时间。说白了,缓存就是让数据在离用户最近的地方待着,别动不动就回老家。
这一章,我们就把HTTP缓存策略彻底讲透。我会从两种缓存模式讲起,再到具体的头部字段,最后聊聊失效和更新。嗯,都是我在线上环境摸爬滚打总结出来的经验。
3.1 强缓存与协商缓存:两种截然不同的思路
HTTP缓存策略,本质上就两种模式:强缓存和协商缓存。它们不是互斥的,而是配合使用的。
- 强缓存:浏览器判断缓存是否过期,如果没过期,直接使用本地缓存,连请求都不发。速度快到飞起,状态码是200(from disk cache 或 from memory cache)。
- 协商缓存:浏览器拿缓存标识去问服务器:“我这个缓存还能用吗?”服务器说“可以”,那就返回304 Not Modified,继续用本地缓存;服务器说“不行”,那就返回200和新资源。
我个人习惯把强缓存理解为“闭门造车”,协商缓存是“请示汇报”。你想想看,如果每次都要请示,那效率肯定不如直接拿来用高。但强缓存也有问题——万一资源更新了,客户端还傻傻地用旧的,那就出事了。
核心结论:强缓存优先于协商缓存。浏览器先检查强缓存,如果命中且未过期,直接返回;否则进入协商缓存流程。
3.2 Cache-Control与Expires:控制强缓存的两把钥匙
强缓存怎么判断过期?靠的就是这两个响应头。
3.2.1 Expires:老派的绝对时间
Expires 是HTTP/1.0时代的产物。它指定一个绝对时间,比如 Expires: Wed, 21 Oct 2025 07:28:00 GMT。浏览器在这个时间之前,都认为缓存有效。
但问题来了——它依赖客户端时间。我曾经遇到过一个线上事故,用户电脑时间被改成了十年前,结果所有资源都强制过期,每次请求都回源,服务器直接被打爆。所以,我个人建议:能不用Expires就别用。
3.2.2 Cache-Control:现代的标准答案
Cache-Control 是HTTP/1.1引入的,它用相对时间,不依赖客户端时钟。常用指令有:
| 指令 | 说明 | 示例 |
|---|---|---|
max-age |
缓存有效时长,单位秒 | max-age=3600(1小时) |
no-cache |
强制走协商缓存,每次都要问服务器 | no-cache |
no-store |
完全不缓存,敏感数据用这个 | no-store |
public |
任何中间节点(CDN、代理)都可以缓存 | public, max-age=86400 |
private |
只有浏览器可以缓存,中间节点不行 | private, max-age=3600 |
举个例子,我常用的静态资源配置:
Cache-Control: public, max-age=31536000, immutable
这里 immutable 是个好东西,告诉浏览器“这个资源永远不会变,放心缓存”。配合文件名哈希(比如 app.a1b2c3.js),效果极佳。
避坑指南:如果同时设置了 Expires 和 Cache-Control,Cache-Control 优先级更高。这是规范规定的,别搞反了。
3.3 ETag与Last-Modified:协商缓存的左右护法
当强缓存失效后,浏览器会带着缓存标识去问服务器。这个标识有两种:
3.3.1 Last-Modified / If-Modified-Since
服务器在响应头里返回 Last-Modified: Fri, 12 Jul 2024 10:30:00 GMT,表示资源最后修改时间。浏览器下次请求时,带上 If-Modified-Since: Fri, 12 Jul 2024 10:30:00 GMT。服务器对比时间,如果没变,返回304。
但这个方法有坑:
- 时间精度只到秒,如果一秒内改了多次,就识别不出来。
- 文件内容没变,但时间变了(比如重新部署),也会触发重新下载。
3.3.2 ETag / If-None-Match
ETag 是资源的唯一标识,通常是对文件内容做哈希(比如MD5)。服务器返回 ETag: "abc123",浏览器下次请求时带上 If-None-Match: "abc123"。服务器对比哈希,一致则返回304。
ETag 比 Last-Modified 更精确。我在项目中遇到过一个问题:用 Last-Modified 时,明明文件没变,只是服务器重启了,结果所有客户端都重新下载了一遍。换成 ETag 后,这个问题就消失了。
注意:ETag 有强验证和弱验证之分。强验证(默认)要求内容完全一致;弱验证(加 W/ 前缀,如 W/"abc123")允许部分差异。一般用强验证就够了。
3.4 缓存失效与更新策略:别让用户看到旧页面
缓存策略定好了,但资源更新了怎么办?这是线上最头疼的问题。我总结了几种常见策略:
3.4.1 文件名哈希法(推荐)
构建工具(Webpack、Vite)会给文件名加上内容哈希。比如 main.7a8b9c.js。内容变了,哈希就变,文件名也变。这样你可以放心设置 max-age=31536000,因为文件名变了,浏览器会当作新资源请求。
这是目前最优雅的方案。我所有的前端项目都这么搞。
3.4.2 版本号法
在URL上加版本号,比如 /v2/main.js 或 /main.js?v=2。但这种方式有个问题:如果只改了一个文件,所有文件版本号都得变,导致缓存大面积失效。
3.4.3 主动失效法
通过CDN或反向代理(比如Nginx)手动清除缓存。比如调用CDN的API刷新某个URL。这适合紧急修复的场景。
我曾经在凌晨两点干过这事——线上有个CSS样式错了,我直接刷新了CDN缓存,三分钟后所有用户都看到了正确的页面。嗯,虽然有点狼狈,但确实管用。
3.5 知识体系总览
下面这张图,是我自己梳理的HTTP缓存决策流程。你看一遍,基本就全通了。
这张图把整个流程串起来了。你从顶部开始,先走强缓存,没过期就直接用;过期了再走协商缓存,问服务器资源变没变。每一步都有对应的HTTP头部在背后工作。
我的建议:对于静态资源(JS、CSS、图片),用强缓存 + 文件名哈希;对于HTML页面,用协商缓存(no-cache)或者短时间强缓存(比如 max-age=300)。这样既保证了性能,又不会让用户看到旧页面。
好了,HTTP缓存策略就讲到这里。记住一句话:缓存不是银弹,但用好了,能让你的服务器轻松十倍。下一章我们聊聊更深入的东西,但今天先把这些基础打牢。