强缓存和协商缓存

强缓存和协商缓存

三葉Leaves Author

浏览器判断缓存策略的决策路径:

1
2
3
4
5
6
7
8
9
10
请求发起 -> 浏览器本地是否有缓存?
├─ 否 ─────────> 向服务器发起普通请求获取数据 (200 OK)

└─ 是 ─────────> 【强缓存阶段】资源是否还在保质期内?
├─ 是(还在保质期) ─> 直接使用本地缓存 (200 OK from cache) ✨ 最快!完全无网络请求!

└─ 否(过期了) ───> 【协商缓存阶段】携带旧文件的标识向服务器发起请求,问服务器文件变没变?
├─ 未改变 ──> 服务器返回 304 Not Modified,浏览器继续使用本地文件 ✨ 有网络请求,但无数据包传输!

└─ 已改变 ──> 服务器返回 200 OK,并带回全新数据和新的缓存规则 ✨ 有网络请求,有全量数据传输!

基础概念

强缓存

  • 判断权在浏览器
  • 命中后不会发 HTTP 请求

可以通过如下响应头判断是否是强缓存:

Expires

形如:

1
Expires: Wed, 18 Mar 2026 10:00:00 GMT

表示:

  • 在这个时间点之前,资源都可以直接使用缓存

  • 过了这个时间点,强缓存失效

这是 HTTP/1.0 的方案,因为其与客户端本地时钟强绑定,所以不稳定、易出错,现已淘汰。

Cache-Control: max-age=xxx

形如:

1
Cache-Control: max-age=3600

表示:

  • 资源在响应返回后的 3600 秒内有效

  • 这 3600 秒内浏览器可以直接走强缓存

这是更灵活、可靠的现代方案,其使用相对时长,不依赖客户端时间。

Cache-Control 的优先级高于Expires

来源分析

在 Chrome DevTools 里你可能看到:

  • HTTP 200

  • from memory cache

  • from disk cache

一个是从内存,一个是从硬盘来的。如下图所示。

图中的前两项都是强缓存,都是从硬盘来的。最后一个 304 ,走的是下面要说的协商缓存


协商缓存

强缓存未命中时,浏览器向服务器发送请求,由服务器判断资源是否发生变化;

  • 如果没变

服务器返回:

1
HTTP/1.1 304 Not Modified

304 的作用不是重新传输资源内容,而是通知浏览器本地缓存仍然有效,因此浏览器会复用本地缓存的实体内容。

  • 如果变了

返回 200 和新资源

和强缓存相比,协商缓存不能由浏览器自己判断,而是要发请求问服务器能不能继续用本地缓存。

协商缓存由两套机制,通常认为 ETag 精度更高,Last-Modified 更轻量。

第一套:基于修改时间(Last-Modified / If-Modified-Since)

  • 响应头:Last-Modified

  • 请求头:If-Modified-Since

服务器第一次返回资源时,告诉浏览器这个资源最后修改时间:

1
Last-Modified: Tue, 10 Mar 2026 08:20:00 GMT

浏览器下次请求这个资源时,会带上:

1
If-Modified-Since: Tue, 10 Mar 2026 08:20:00 GMT

服务器拿这个时间和当前资源的最后修改时间比对:

  • 如果资源没变,返回 304

  • 如果资源变了,返回 200 和新资源

优点

简单,性能开销小

缺点
  • 时间精度有限,应付不了一秒修改多次的情况
  • 如果文件重新部署、重新生成,修改时间会变,但内容没变

第二套:基于内容标识(ETag / If-None-Match)

  • 响应头:ETag

  • 请求头:If-None-Match

服务器第一次返回资源时,会给它一个标识,形如:

1
ETag: "abc123xyz"

这个标识通常基于:

  • 文件内容

  • 文件版本号

  • inode + mtime + size

  • 或服务端自定义算法

其实很可能就是对文件做的 hash。

浏览器下次请求时带上:

1
If-None-Match: "abc123xyz"

服务器比较当前资源的 ETag:

  • 一样:说明资源没变,返回 304

  • 不一样:说明资源变了,返回 200 和新资源

能否同时存在?

服务端生成和比较 ETag 会有额外开销。因此实际中协商缓存的这两种机制两者可以配合使用,优先以 ETag 为准。
值得一提的是,强缓存和协商缓存也可以同时存在,只是强缓存优先级更高,过期了才会走协商缓存。

强 ETag 和 弱 ETag

刚才说的都是强 Etag ,形如下面这个是弱 Etag:

1
ETag: W/"abc123"

弱 ETag 常见于某些动态生成内容或代理场景中,用来降低严格比较带来的成本。表示“语义上相同即可”,不要求字节完全一致。

比如:

  • 压缩方式不同

  • 某些不影响语义的格式变化

  • 响应序列化细节不同

仍然可以认为是“同一个资源版本”。


Cache-Control 的常见值

max-age

1
Cache-Control: max-age=31536000

用于强缓存,表示在指定秒数内可以直接使用资源。例如上面这个表示缓存一年。

no-cache

1
Cache-Control: no-cache

禁用强缓存。

这个很有误导性,它不是不允许缓存,而是 不允许直接使用强缓存,必须协商确认后才能用。

no-store

这才是真正意义的“不缓存”

1
Cache-Control: no-store
  • 不允许缓存响应内容

  • 浏览器、代理、中间缓存都不应该存储这个响应

这适合:

  • 敏感信息

  • 银行页面

  • 一次性数据

  • 隐私场景

public

表示响应可以被任何缓存保存,包括:

  • 浏览器本地缓存

  • CDN

  • 代理服务器

private

表示响应只能被客户端私有缓存保存,通常是浏览器,不应该被共享缓存保存。

这在涉及用户个性化内容时很常见。

immutable

浏览器遇到这个,一些场景下连协商确认都可以更激进地省掉。

这适合“版本化、内容稳定、URL 一变就代表新资源”的文件,例如:

  • app.8d3f1c.js

  • style.a12bc9.css

  • vendor.993311.js

因为这些资源名带 hash,本来就应该是“内容不可变”的。

启发式缓存(Heuristic Cache)

这种方式可控性和一致性都比较差,因此工程实践中一般不建议依赖它,而是应该显式配置缓存策略

承接上文。

  • 进入强缓存的条件:有 max-age 或者 Expires。
  • 进入协商缓存的条件:强缓存实效。

可是如果没有强缓存的条件,但是却有一个 Last-Modified,这是怎么回事?例如下面这个情况:

1
2
3
HTTP/1.1 200 OK
Last-Modified: Tue, 10 Mar 2026 08:00:00 GMT
Content-Type: text/css

这就要说到启发式缓存了:

1
2
3
4
5
6
7
浏览器拿到响应
├─ 服务端明确给了强缓存头(Cache-Control / Expires)
│ └─ 按显式强缓存规则处理
├─ 强缓存失效后有协商标识(ETag / Last-Modified)
│ └─ 走协商缓存
└─ 如果没有明确缓存策略
└─ 浏览器可能根据启发式规则推断缓存行为(Heuristic Cache)

启发缓存,指的是当响应里没有明确给出完整的缓存策略时,浏览器会根据 HTTP 规范允许的启发式方式来推断这个资源可以缓存多久。

Heuristic Cache 更接近强缓存。在推断有效期内可能直接用本地缓存,这更接近“浏览器自己推导出来的强缓存行为”,不同浏览器有自己的实现,所以不是很稳定。

最经典的启发式依据就是:

  • 响应里有 Last-Modified

  • 浏览器会拿“当前时间 - 最后修改时间”算出资源年龄

  • 再取其中一个比例,作为一个临时缓存时间

一个常见的经验性规则是:

缓存时间可能取资源年龄的一小部分,比如 10% 左右
(这是常见启发式思路,不要把它当成绝对统一标准)

例如:

  • 资源最后修改于 10 天前

  • 浏览器可能推一个较短缓存期,比如约 1 天左右

工程实践

在实际项目中,通常 HTML 不做长时间强缓存,而是采用协商缓存保证入口及时更新;JS、CSS、图片等静态资源会配合文件名 hash 使用长期强缓存,这样既能充分利用缓存,又能在资源变更时通过新 URL 让浏览器拉取最新文件。

HTML / JS / CSS 的不同缓存策略

HTML

HTML 是入口,要引用不用的资源,例如:

1
2
<link rel="stylesheet" href="/style.a12bc9.css">
<script src="/app.8d3f1c.js"></script>

所以 HTML 的核心要求不是“极致缓存命中”,而是:

入口必须尽可能及时地发现更新。

因此实际中常见策略是:协商缓存

JS / CSS

现代最常见的策略是:文件名 hash + 长时间的强缓存 + immutable

现代框架构建的 JS / CSS 中往往带 hash :

1
2
/app.8d3f1c.js
/style.a12bc9.css

如果内容不变,URL 不变;内容一变,URL 变。

这时最适合的策略就是:

1
Cache-Control: max-age=31536000, immutable

图片 / 静态资源

逻辑其实和 JS / CSS 差不多,也可以用 hash 做文件名然后长期强缓存:

1
2
3
- logo.23dd9a.png

- iconfont.98ad2f.woff2

浏览器处理不同场景采用的缓存策略

具体行为会受到浏览器实现和开发者工具设置影响,下面的结论只能表示一种 ‘倾向’。

1. 地址栏回车 / 新开标签访问

走标准流程,见本文顶部的图。

2. 普通刷新(F5 / 点击刷新)

取决于浏览器实现,通常会倾向于跳过强缓存,但保留协商缓存能力

普通刷新通常会让浏览器更倾向于进行协商校验,而不是直接复用强缓存,因此经常会触发 304。

3. 强制刷新(Ctrl + F5 / Cmd + Shift + R)

强制刷新通常会绕过缓存,直接重新向服务器请求资源。

这就是为什么有时候你改了资源,普通刷新还感觉“没更新”,但强刷就好了。

Nginx 配置实例

缓存策略一般都是服务器那边配置的。不同的服务器都有自己的实现,以 Nginx 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name example.com;

root /usr/share/nginx/html;

location ~* \.html$ {
add_header Cache-Control "no-cache";
}

location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

location / {
try_files $uri $uri/ /index.html;
}
}

这个配置就践行了前面提到的最佳实践:

[[#HTML / JS / CSS 的不同缓存策略]]

  • 标题: 强缓存和协商缓存
  • 作者: 三葉Leaves
  • 创建于 : 2026-03-11 00:00:00
  • 更新于 : 2026-03-16 12:05:05
  • 链接: https://blog.oksanye.com/e8c127d084a3/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论