浏览器缓存通关指南:强制缓存、协商缓存,一次性讲透

缓存这东西,到底难在哪

做前端的人,迟早会被缓存坑一回。

最常见的情节:改完代码上线,用户打开页面还是旧的。清缓存、硬刷新、隐身模式,一顿操作猛如虎,一看页面还是个二百五。

反过来也有问题——明明资源没变,用户每次打开都重新下载,页面加载慢得像在拨号上网。

这个问题背后的关键就是:浏览器缓存策略

理解它并不难,但你得搞清楚两件事——强制缓存协商缓存。这两个概念搞懂了,缓存问题就解决了一大半。

浏览器是怎么决定要不要用缓存的

浏览器每次请求资源,会按照一个固定的优先级来走:

  1. 先查强制缓存 —— 命中就直接用本地副本,不发请求
  2. 强制缓存没命中,走协商缓存 —— 带上标识去问服务器,“我这个版本还能用吗?”
  3. 协商缓存也没命中 —— 老老实实发完整请求,下载最新资源

整个过程像三层过滤网。第一层最快,第三层最慢。

下面我一个个拆开来说。

强制缓存(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.js
  • style.7c2d1e.css
  • logo.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-ControlLast-ModifiedETag

4. 硬刷新

  • Cmd/Ctrl + R:普通刷新,协商缓存依然生效
  • Cmd/Ctrl + Shift + R:硬刷新,强制走网络
  • DevTools 打开时长按刷新按钮也可以选

总结

特性 强制缓存 协商缓存
发请求? 不发 发,但只传头部
控制头 Cache-Control, Expires ETag, Last-Modified
速度快 极快 依然很快
适合 hash 资源、静态图片 HTML、不确定是否变化的资源

优化的核心思路很简单:

不变的资源扔强制缓存,可能变的资源走协商缓存。

实际项目中对照这个原则配就对了。配好之后,页面加载速度和用户体验都会有肉眼可见的提升。