浏览器缓存通关指南:强制缓存、协商缓存,一次性讲透
缓存这东西,到底难在哪
做前端的人,迟早会被缓存坑一回。
最常见的情节:改完代码上线,用户打开页面还是旧的。清缓存、硬刷新、隐身模式,一顿操作猛如虎,一看页面还是个二百五。
反过来也有问题——明明资源没变,用户每次打开都重新下载,页面加载慢得像在拨号上网。
这个问题背后的关键就是:浏览器缓存策略。
理解它并不难,但你得搞清楚两件事——强制缓存和协商缓存。这两个概念搞懂了,缓存问题就解决了一大半。
浏览器是怎么决定要不要用缓存的
浏览器每次请求资源,会按照一个固定的优先级来走:
- 先查强制缓存 —— 命中就直接用本地副本,不发请求
- 强制缓存没命中,走协商缓存 —— 带上标识去问服务器,“我这个版本还能用吗?”
- 协商缓存也没命中 —— 老老实实发完整请求,下载最新资源
整个过程像三层过滤网。第一层最快,第三层最慢。
下面我一个个拆开来说。
强制缓存(Strong Cache)
什么时候会发生
强制缓存是浏览器最激进的缓存策略。只要你访问过的资源还在有效期内,浏览器根本不会往服务器发请求。
打开 Chrome DevTools 的 Network 面板,看到请求的 Size 列显示 (from disk cache) 或 (from memory cache),那就是强制缓存命中了。
控制它的 HTTP 头
Expires(HTTP/1.0)
最老的方案,给一个绝对过期时间:
Expires: Wed, 03 Jun 2027 04:00:00 GMT
问题很明显:依赖客户端时间。用户改了系统时间,缓存立马乱套。
Cache-Control(HTTP/1.1,推荐)
现代浏览器都用这个。常见指令:
Cache-Control: max-age=3600
告诉浏览器:这个资源在 3600 秒内是新鲜的,别来问。
还有一些常用组合:
| 指令 | 含义 |
|---|---|
max-age=86400 |
缓存一天 |
no-cache |
不使用强制缓存(但协商缓存仍可用) |
no-store |
完全禁止缓存 |
public |
CDN、代理等都可以缓存 |
private |
只有浏览器可以缓存 |
immutable |
资源永不变,刷新页面也不用再验证 |
Expires vs Cache-Control
两者同时出现时,Cache-Control 优先级更高。实际项目中只配 Cache-Control 就够了。
什么资源适合强制缓存
不会变、或者文件名带 hash 的资源。
比如打包工具生成的:
app.a3b8f9.jsstyle.7c2d1e.csslogo.1a2b3c.png
只要 hash 变了就是新文件,直接设 Cache-Control: max-age=31536000(一年),贼省事。
协商缓存(Negotiation Cache / Conditional Cache)
什么时候会发生
强制缓存过期了,或者你明确声明了 no-cache。
这时候浏览器不会直接用本地副本,而是带着资源的"身份标识"去问服务器:“我这个版本还能用吗?”
服务器说"能用" → 返回 304 Not Modified,不传文件本体,只传几个头部。
服务器说"不能用了" → 返回 200,传新文件。
控制它的 HTTP 头
1. Last-Modified / If-Modified-Since(HTTP/1.0)
服务器第一次返回时带 Last-Modified:这个文件最后一次修改的时间。
Last-Modified: Tue, 02 Jun 2026 10:30:00 GMT
后续请求浏览器带上 If-Modified-Since:
If-Modified-Since: Tue, 02 Jun 2026 10:30:00 GMT
服务器对比时间,没变就 304。
缺点:
- 时间精度只到秒。一秒内改了两次?服务器对比不出来。
- 文件改了但内容没变?也会触发重新下载。
2. ETag / If-None-Match(HTTP/1.1,推荐)
服务器根据文件内容生成一个唯一标识(类似 hash):
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
后续请求浏览器带上 If-None-Match:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
服务器对比 ETag,一致就 304。
优点: 内容没变一定不重新下载,内容变了也不会漏掉。
ETag 有强弱之分:
- 强 ETag:
"abc123"—— 字节级比对,CDN 常用 - 弱 ETag:
W/"abc123"—— 语义等价的资源也可视为未变
Last-Modified 和 ETag 同时出现
ETag 优先级更高。服务器会优先用 ETag 做比对。
一张图看清完整流程
用户访问页面
│
▼
有强制缓存? ──是──→ (from disk/memory cache) ✅
│否
▼
走协商缓存
│
▼
服务器返回 304? ──是──→ 用本地缓存 ✅
│否
▼
服务器返回 200 + 新资源
真实场景:一个页面加载到底走了哪些缓存
来拆一个典型页面:
HTML
Cache-Control: no-cache
HTML 不设强制缓存。每次请求都走协商缓存。这样你更新了页面,用户下次打开必然能拿到最新版。304 只传头部,成本很低。
JS/CSS(带 hash)
Cache-Control: public, max-age=31536000, immutable
一年强缓存,加了 immutable 表示就算用户手动刷新也不需要重新验证。只有发布新版本时改 hash,浏览器才会下载新文件。
图片
Cache-Control: public, max-age=2592000
30 天强缓存。图片一般不频繁变动,设个合理时间就行。
API 接口
Cache-Control: no-store
涉及数据变更的接口通常完全不允许缓存,或者按场景设一个很短的 max-age。
最容易踩的坑
坑一:改了 HTML 没改文件名
HTML 走强缓存设了 max-age=3600。你改了 HTML 上线,用户在一小时内看到的全是旧版。
解决:HTML 不要设强缓存,或者用 no-cache。
坑二:ETag 和 Last-Modified 混用出 bug
某些反向代理对 ETag 处理有 bug,导致一直返回 200。这时候可以只保留 Last-Modified,或者升级代理版本。
坑三:Service Worker 和 HTTP 缓存打架
Service Worker 本身就是一个额外的缓存层。如果你同时用了 SW 和 HTTP 缓存,得注意它们的交互顺序。SW 先拦截请求,再决定走网络还是走 HTTP 缓存。
坑四:CDN 和源站缓存策略不一致
很多人在源站没配缓存,但 CDN 配了。结果你清了 CDN 缓存,CDN 重新回源时源站返回 304,CDN 又拿不到新版本。
解决:回源请求配上合适的缓存控制,确保 CDN 能正确回源拉新。
怎么调试缓存问题
1. 打开 DevTools → Network 面板
看 Size 列:
(from disk cache)/(from memory cache)→ 强制缓存命中<304>→ 协商缓存命中
2. 禁用缓存
DevTools → Network → 勾选 “Disable cache”。开发调试时一定要开这个,不然改了代码看不到效果。
3. 查看响应头
点开具体请求,看 Response Headers 里的 Cache-Control、Last-Modified、ETag。
4. 硬刷新
Cmd/Ctrl + R:普通刷新,协商缓存依然生效Cmd/Ctrl + Shift + R:硬刷新,强制走网络- DevTools 打开时长按刷新按钮也可以选
总结
| 特性 | 强制缓存 | 协商缓存 |
|---|---|---|
| 发请求? | 不发 | 发,但只传头部 |
| 控制头 | Cache-Control, Expires | ETag, Last-Modified |
| 速度快 | 极快 | 依然很快 |
| 适合 | hash 资源、静态图片 | HTML、不确定是否变化的资源 |
优化的核心思路很简单:
不变的资源扔强制缓存,可能变的资源走协商缓存。
实际项目中对照这个原则配就对了。配好之后,页面加载速度和用户体验都会有肉眼可见的提升。