缓存解决的是什么问题
一个页面的静态资源通常有几百 KB 到几 MB。如果每次访问都重新下载,既浪费带宽又拖慢首屏。
HTTP 缓存的核心思路是:让浏览器在本地留一份副本,下次直接复用。难点在于——什么时候可以放心复用。
强缓存:直接用本地副本
强缓存命中时,浏览器根本不发请求,直接从本地读。控制它的是两个响应头:
| 响应头 | 含义 | 问题 |
|---|---|---|
Expires | 绝对时间点 | 依赖客户端时钟,不准 |
Cache-Control | 相对秒数 | 现在的标准做法 |
Cache-Control: max-age=31536000, immutable
max-age 是秒数,上面这行表示一年内直接读本地。immutable 是一个额外承诺:这个资源在这一年内绝不会变,浏览器连用户按刷新按钮时都不会去校验。
immutable 只能用在文件名带内容哈希的资源上,用错会导致用户永远拿不到更新。
协商缓存:问一下有没有变
强缓存过期后,浏览器会带上一对标识去问服务器「这个还能用吗」。服务器如果回答「没变」,返回 304,响应体为空,只省下传输时间。
| 请求头 | 响应头 | 依据 |
|---|---|---|
If-None-Match | ETag | 内容指纹,精确 |
If-Modified-Since | Last-Modified | 修改时间,秒级精度 |
ETag 更可靠,因为:
- 时间只精确到秒,一秒内改两次会漏掉
- 文件被重新生成但内容没变时,时间变了,会误判为「已修改」
实践中两个都带上,服务器优先看 ETag。
内容哈希文件名是最优解
前面两种机制都有取舍。现代前端工程给出的答案是:把内容哈希写进文件名。
app.a3f9c1.js ← 内容变了就变成 app.b7e2d4.js
这样每个文件的内容和名字一一对应,于是可以:
- 对带哈希的文件设一年强缓存 + immutable,永不校验
- 对入口 HTML 设
no-cache,保证每次拿到最新的哈希引用
结果是:发版后用户只下载真正变化的那几个文件,其余全部命中本地缓存。
各类资源的缓存策略
| 资源类型 | 建议策略 | 理由 |
|---|---|---|
| 带哈希的 JS / CSS | max-age=31536000, immutable | 内容与名字绑定,可以永久缓存 |
| 入口 HTML | no-cache | 必须每次校验,否则拿不到新哈希 |
| 头像、封面图 | max-age=2592000(30 天) | 变动不频繁 |
| API 接口 | no-store 或短 max-age | 数据实时性优先 |
| 字体文件 | max-age=31536000 | 几乎不变 |
注意 no-cache 和 no-store 的区别,很多人会搞混:
no-cache:可以缓存,但每次使用前必须校验no-store:完全不许缓存,连存都不存
入口 HTML 应该用 no-cache 而不是 no-store,因为缓存下来配合 304 校验其实更快。
一个容易踩的坑:add_header 的继承
Nginx 里 add_header 有个反直觉的规则:子级 location 只要自己写了任何一条 add_header,父级的全部失效。
server {
add_header X-Frame-Options "DENY";
location /static/ {
add_header Cache-Control "max-age=31536000";
# 这里 X-Frame-Options 不会生效!
}
}
解决办法是在每个 location 里重复写一遍安全头。虽然啰嗦,但这是 Nginx 的既定行为。
怎么排查缓存问题
浏览器开发者工具的 Network 面板里,Size 一列会直接告诉你结果:
| 显示 | 含义 |
|---|---|
(memory cache) | 强缓存命中,没发请求 |
(disk cache) | 强缓存命中,从磁盘读 |
304 Not Modified | 协商缓存命中 |
| 正常大小 | 缓存未命中,完整下载 |
如果改了代码但页面没变,先看这里。九成情况是强缓存还没过期,而不是代码没生效。
小结
HTTP 缓存的完整策略可以总结成一句话:带哈希的资源永久缓存,入口 HTML 每次校验,接口数据不缓存。
把这三类分开处理,既能让用户秒开,又不会出现「改了代码看不到」的问题。