强缓存和协商缓存
浏览器判断缓存策略的决策路径:
1 | 请求发起 -> 浏览器本地是否有缓存? |
基础概念
强缓存
- 判断权在浏览器
- 命中后不会发 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 | 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 | 200 OK |
这就要说到启发式缓存了:
1 | 浏览器拿到响应 |
启发缓存,指的是当响应里没有明确给出完整的缓存策略时,浏览器会根据 HTTP 规范允许的启发式方式来推断这个资源可以缓存多久。
Heuristic Cache 更接近强缓存。在推断有效期内可能直接用本地缓存,这更接近“浏览器自己推导出来的强缓存行为”,不同浏览器有自己的实现,所以不是很稳定。
最经典的启发式依据就是:
-
响应里有
Last-Modified -
浏览器会拿“当前时间 - 最后修改时间”算出资源年龄
-
再取其中一个比例,作为一个临时缓存时间
一个常见的经验性规则是:
缓存时间可能取资源年龄的一小部分,比如 10% 左右
(这是常见启发式思路,不要把它当成绝对统一标准)
例如:
-
资源最后修改于 10 天前
-
浏览器可能推一个较短缓存期,比如约 1 天左右
工程实践
在实际项目中,通常 HTML 不做长时间强缓存,而是采用协商缓存保证入口及时更新;JS、CSS、图片等静态资源会配合文件名 hash 使用长期强缓存,这样既能充分利用缓存,又能在资源变更时通过新 URL 让浏览器拉取最新文件。
HTML / JS / CSS 的不同缓存策略
HTML
HTML 是入口,要引用不用的资源,例如:
1 | <link rel="stylesheet" href="/style.a12bc9.css"> |
所以 HTML 的核心要求不是“极致缓存命中”,而是:
入口必须尽可能及时地发现更新。
因此实际中常见策略是:协商缓存
JS / CSS
现代最常见的策略是:文件名 hash + 长时间的强缓存 + immutable
现代框架构建的 JS / CSS 中往往带 hash :
1 | /app.8d3f1c.js |
如果内容不变,URL 不变;内容一变,URL 变。
这时最适合的策略就是:
1 | Cache-Control: max-age=31536000, immutable |
图片 / 静态资源
逻辑其实和 JS / CSS 差不多,也可以用 hash 做文件名然后长期强缓存:
1 | - logo.23dd9a.png |
浏览器处理不同场景采用的缓存策略
具体行为会受到浏览器实现和开发者工具设置影响,下面的结论只能表示一种 ‘倾向’。
1. 地址栏回车 / 新开标签访问
走标准流程,见本文顶部的图。
2. 普通刷新(F5 / 点击刷新)
取决于浏览器实现,通常会倾向于跳过强缓存,但保留协商缓存能力。
普通刷新通常会让浏览器更倾向于进行协商校验,而不是直接复用强缓存,因此经常会触发 304。
3. 强制刷新(Ctrl + F5 / Cmd + Shift + R)
强制刷新通常会绕过缓存,直接重新向服务器请求资源。
这就是为什么有时候你改了资源,普通刷新还感觉“没更新”,但强刷就好了。
Nginx 配置实例
缓存策略一般都是服务器那边配置的。不同的服务器都有自己的实现,以 Nginx 为例:
1 | server { |
这个配置就践行了前面提到的最佳实践:
[[#HTML / JS / CSS 的不同缓存策略]]
- 标题: 强缓存和协商缓存
- 作者: 三葉Leaves
- 创建于 : 2026-03-11 00:00:00
- 更新于 : 2026-03-12 11:20:29
- 链接: https://blog.oksanye.com/e8c127d084a3/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。