一、缓存的两大类型
HTTP 缓存分为两种:强缓存 和 协商缓存。
| 类型 | 是否请求服务器 | 返回状态码 | 控制字段 |
|---|---|---|---|
| 强缓存 | ❌ 不请求 | 200 (from cache) | Cache-Control / Expires |
| 协商缓存 | ✅ 请求,但只发头部 | 304 Not Modified | ETag / Last-Modified |
二、强缓存
浏览器直接从本地读取资源,不发任何请求给服务器。
控制字段
Cache-Control(推荐,HTTP/1.1)
Cache-Control: max-age=86400
| 值 | 含义 |
|---|---|
max-age=N | 资源在 N 秒内有效,直接使用缓存 |
no-cache | 不使用强缓存,每次都走协商缓存验证 |
no-store | 完全不缓存,每次都重新请求 |
public | 可被代理服务器缓存 |
private | 只能被浏览器缓存,不允许代理缓存 |
immutable | 资源永远不会变,告知浏览器不要发条件请求 |
Expires(旧,HTTP/1.0)
Expires: Wed, 01 Jan 2026 00:00:00 GMT
用绝对时间控制过期,缺点是依赖本地时钟,与服务器时间可能不一致。Cache-Control 优先级高于 Expires。
流程图
浏览器发起请求
│
▼
本地有缓存? ──否──▶ 发请求到服务器
│
是
│
▼
缓存未过期(max-age 内)?
│ │
是 否
│ │
▼ ▼
直接使用缓存 走协商缓存流程
200 (from cache)
三、协商缓存
强缓存失效后,浏览器带着验证标识去问服务器:我这份缓存还有效吗?
- 服务器说"还有效" → 返回 304 Not Modified,浏览器用本地缓存
- 服务器说"变了" → 返回 200,携带新资源
方案一:Last-Modified / If-Modified-Since
第一次请求,服务器响应:
Last-Modified: Tue, 04 Mar 2025 10:00:00 GMT
后续请求,浏览器携带:
If-Modified-Since: Tue, 04 Mar 2025 10:00:00 GMT
服务器比较时间,未修改则返回 304。
缺陷:
- 精度只到秒级,1秒内多次修改无法感知
- 文件内容没变但 mtime 变了,也会被认为"已修改"(如重新部署、touch 操作)
方案二:ETag / If-None-Match(推荐)
ETag 是服务器根据文件内容生成的唯一标识符(类似内容指纹/哈希),只要内容不变,ETag 就不变。
第一次请求,服务器响应:
ETag: "abc123def456"
Cache-Control: no-cache
后续请求,浏览器携带:
If-None-Match: "abc123def456"
服务器逻辑:
If-None-Match 的值 == 当前资源 ETag?
│ │
是 否
│ │
▼ ▼
304 Not Modified 200 + 新资源 + 新 ETag
(浏览器用本地缓存)
ETag 的两种类型
| 类型 | 格式 | 含义 |
|---|---|---|
| 强验证 | "abc123" | 字节级完全一致才匹配 |
| 弱验证 | W/"abc123" | 语义相同即可匹配(内容基本相同,允许微小差异) |
ETag vs Last-Modified 对比
| 对比项 | Last-Modified | ETag |
|---|---|---|
| 精度 | 秒级 | 内容级(哈希) |
| 内容未变但时间变了 | ❌ 误判为已修改 | ✅ 正确命中缓存 |
| 内容变了但时间没变 | ❌ 误判为未修改 | ✅ 正确返回新资源 |
| 服务器性能开销 | 低 | 略高(需计算哈希) |
| 优先级 | 低 | 高(两者共存时优先用 ETag) |
两者可以同时使用,优先使用 ETag 验证。
四、强制刷新
浏览器提供了几种方式绕过缓存,行为各不相同。
普通访问(正常导航)
- 地址栏回车 / 点击链接
- 走完整缓存流程:先强缓存,再协商缓存
普通刷新(F5 / Cmd+R)
- 浏览器跳过强缓存,直接发请求
- 请求头会带上
Cache-Control: max-age=0或If-None-Match/If-Modified-Since - 服务器可能返回 304
请求头示例:
Cache-Control: max-age=0
If-None-Match: "abc123"
If-Modified-Since: ...
强制刷新(Ctrl+Shift+R / Cmd+Shift+R)
- 浏览器完全绕过本地缓存
- 请求头带上:
Cache-Control: no-cache
Pragma: no-cache
- 不携带
If-None-Match等验证头 - 服务器必须返回完整资源(200)
三种刷新方式对比
| 方式 | 强缓存 | 协商缓存 | 结果 |
|---|---|---|---|
| 普通访问 | ✅ 使用 | 过期后使用 | 200 from cache / 304 |
| F5 刷新 | ❌ 跳过 | ✅ 使用 | 304 或 200 |
| Ctrl+Shift+R | ❌ 跳过 | ❌ 跳过 | 必定 200 |
五、完整请求流程总结
浏览器发起请求
│
▼
强制刷新? ──是──▶ 直接请求服务器 ──▶ 200 返回完整资源
│
否
│
▼
本地有缓存 且 未过期(强缓存命中)?
│ │
是 否
│ │
▼ ▼
200 (from cache) 携带 ETag/Last-Modified 请求服务器
│
服务器对比资源是否变化
│ │
否 是
│ │
▼ ▼
304 Not 200 + 新资源
Modified
(用本地缓存)
六、实践建议
HTML 文件
Cache-Control: no-cache
HTML 是入口,需要每次验证,避免用户拿到旧版页面。
JS / CSS / 图片(带 hash 的文件名)
Cache-Control: max-age=31536000, immutable
如 app.a1b2c3d4.js,内容变化时文件名也变,所以可以永久缓存。
API 接口
Cache-Control: no-store
动态数据不应被缓存。
七、常见面试题速答
Q:ETag 和 Last-Modified 有什么区别?
ETag 基于内容哈希,精度更高;Last-Modified 基于时间,存在秒级误差和时间漂移问题。两者共存时优先使用 ETag。
Q:304 和 200 from cache 有什么区别?
200 from cache 是强缓存命中,完全不发网络请求;304 是协商缓存命中,发了请求但服务器告知"未修改",响应体为空。
Q:no-cache 和 no-store 有什么区别?
no-cache不是"不缓存",而是"不使用强缓存,每次都要协商验证";no-store才是真正的不存储任何缓存。
Q:为什么强制刷新后还是旧资源?
强制刷新只针对浏览器本地缓存,如果中间有 CDN 或代理服务器,CDN 层的缓存不会被清除,需要手动刷新 CDN。