浏览器缓存机制
面试一句话总结:浏览器缓存分为强缓存和协商缓存。强缓存优先,命中则直接从本地读取(状态码 200 from cache);若强缓存失效,则向服务器发起协商缓存请求,若资源未更改则返回 304,否则返回新资源 (200)。在工程化中,HTML 走协商缓存,带 Hash 的静态资源走长效强缓存。
1. 缓存位置(按查找优先级)
高级面试中,面试官可能会问:“除了 HTTP 缓存,你还知道哪些缓存?”
- Service Worker:运行在浏览器背后的独立线程,可实现自由控制缓存(PWA 的核心)。
- Memory Cache(内存缓存):读取极快,持续性短,随着进程(Tab 页)关闭而释放。通常用于 Base64 图片和较小的 JS/CSS。
- Disk Cache(硬盘缓存):读取稍慢,容量大,持续性长。通常用于较大的 JS/CSS 等大文件。
- Push Cache(推送缓存):HTTP/2 中的新特性,只在会话(Session)中存在。
2. 强缓存 (Local Cache)
强缓存不会向服务器发送请求,直接从缓存中读取资源,状态码为 200 (from memory cache 或 from disk cache)。
Cache-Control(HTTP/1.1,绝对核心):max-age=<seconds>:设置缓存最大有效时间。no-cache:(易错点!) 它不是不缓存,而是跳过强缓存,强制进入协商缓存。no-store:真正的完全不缓存,每次都向服务器请求完整资源。s-maxage:针对代理服务器(CDN)的缓存时间,优先级高于max-age。immutable:明确告诉浏览器该文件永不改变,连用户按 F5 刷新都不会发起请求。
Expires(HTTP/1.0,逐渐淘汰):- 值为一个绝对的服务器时间(如
Wed, 21 Oct 2025 07:28:00 GMT)。 - 缺点:严重依赖本地时钟。如果客户端修改了本地时间,缓存控制就会失效。
- 值为一个绝对的服务器时间(如
优先级:Cache-Control > Expires。
3. 协商缓存 (Negotiated Cache)
当强缓存失效,或者设置了 Cache-Control: no-cache 时,浏览器会携带缓存标识向服务器发起请求。
如果命中协商缓存,服务器返回 304 Not Modified(空响应体),告诉浏览器去读本地缓存。
ETag/If-None-Match(优先级更高)ETag是服务器生成的资源唯一 Hash 值(只要文件内容变了,Hash 就变)。- 浏览器下次请求时,通过
If-None-Match带上这个 Hash,服务器进行比对。
Last-Modified/If-Modified-Since- 服务器返回资源的最后修改时间。
- 浏览器下次请求通过
If-Modified-Since带上这个时间,服务器比对最后修改时间。
面试高频:为什么有了 Last-Modified 还需要 ETag?
- 精度问题:
Last-Modified的时间精度是秒。如果在一秒内修改了文件,Last-Modified无法感知,会导致返回旧缓存。- 内容不变问题:有时候文件只是被重新生成(修改时间变了),但内容根本没变。此时 ETag 依然不变,可以精准命中缓存,而
Last-Modified会认为资源已过期。
优先级:ETag > Last-Modified。
4. 实战:现代前端工程中的缓存策略
在 Webpack / Vite 构建的项目中,我们通常采用如下最佳实践:
① HTML 文件:协商缓存(或不缓存)
- 策略:外网 C 端应用通常使用
Cache-Control: no-cache(协商缓存);内部后台或对更新实时性要求极高的应用,可直接使用Cache-Control: no-store(彻底不缓存)。 - 原因:HTML 是入口,绝对不能被强缓存!如果 HTML 被强缓存,当发布新版时,浏览器还在用旧的 HTML,就不会去请求新的 JS/CSS,导致页面永远不更新。
② 静态资源 (JS/CSS/图片):长效强缓存
- 策略:
Cache-Control: max-age=31536000, immutable(缓存 1 年)。 - 原因:现代构建工具会在静态资源的文件名中注入内容 Hash(如
app.a3b4c5.js)。内容一旦改变,文件名一定会变。旧的 HTML 引用旧文件,新的 HTML 引用新文件,完全不存在缓存更新冲突。因此,带 Hash 的资源可以直接永久强缓存。
③ 无 Hash 的图片 / 业务 API 接口:视情况而定
- 策略:一般使用协商缓存(
no-cache)或较短时间的强缓存(如max-age=86400即 1 天)。 - 原因:对于用户头像(如
avatar.png)或动态数据,内容可能会随时改变但 URL 不变,为了保证及时更新,通常采用协商缓存。
5. 面试常见追问
Q1:no-cache 和 max-age=0 有什么区别?
no-cache:强制进行协商缓存。每次请求前必须向服务器验证资源是否被更改。如果你不想将 response 存储在缓存中,需要使用no-store。max-age=0:告诉浏览器该资源已立即过期,这通常也会触发浏览器去发送协商缓存请求。在绝大多数情况下表现一致,但no-cache的语义更明确。
Q2:用户行为对缓存的影响?
- 地址栏输入 URL 回车 / 点击链接:正常走缓存策略(强缓存有效则走强缓存)。
- F5 / 点击刷新按钮:跳过强缓存,直接发起协商缓存(请求头带上
Cache-Control: max-age=0)。 - Ctrl + F5 (强制刷新):跳过所有缓存(强缓存和协商缓存),直接向服务器请求最新资源。浏览器会在请求头中强制带上
Cache-Control: no-cache和Pragma: no-cache,并且不会携带If-Modified-Since和If-None-Match字段。因为没有了这两个条件验证字段,服务器就无法进行协商缓存的比对,只能返回 200 和完整的最新资源。
Q3:分布式系统 (多台服务器) 中的协商缓存坑点?
如果开启了多台机器的负载均衡:
- Last-Modified 坑点:多台机器上同一个文件的修改时间可能不一致。
- ETag 坑点:默认情况下,Nginx 等服务器生成的 ETag 包含了
inode(文件索引节点),不同机器上的inode是不同的。这就导致即使文件内容完全一致,请求分发到不同机器时 ETag 也会不同,从而导致缓存失效。解决方案:在分布式系统中,通常会配置 Nginx 关闭 ETag 的inode计算,仅根据文件大小和修改时间生成,或者干脆统一由构建工具(而不是 Nginx 默认策略)去控制强缓存,规避协商缓存的短板。