Skip to main content

浏览器缓存机制

面试一句话总结:浏览器缓存分为强缓存协商缓存。强缓存优先,命中则直接从本地读取(状态码 200 from cache);若强缓存失效,则向服务器发起协商缓存请求,若资源未更改则返回 304,否则返回新资源 (200)。在工程化中,HTML 走协商缓存,带 Hash 的静态资源走长效强缓存。

1. 缓存位置(按查找优先级)

高级面试中,面试官可能会问:“除了 HTTP 缓存,你还知道哪些缓存?”

  1. Service Worker:运行在浏览器背后的独立线程,可实现自由控制缓存(PWA 的核心)。
  2. Memory Cache(内存缓存):读取极快,持续性短,随着进程(Tab 页)关闭而释放。通常用于 Base64 图片和较小的 JS/CSS。
  3. Disk Cache(硬盘缓存):读取稍慢,容量大,持续性长。通常用于较大的 JS/CSS 等大文件。
  4. 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?

  1. 精度问题Last-Modified 的时间精度是。如果在一秒内修改了文件,Last-Modified 无法感知,会导致返回旧缓存。
  2. 内容不变问题:有时候文件只是被重新生成(修改时间变了),但内容根本没变。此时 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-cachemax-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-cachePragma: no-cache,并且不会携带 If-Modified-SinceIf-None-Match 字段。因为没有了这两个条件验证字段,服务器就无法进行协商缓存的比对,只能返回 200 和完整的最新资源。

Q3:分布式系统 (多台服务器) 中的协商缓存坑点?

如果开启了多台机器的负载均衡:

  1. Last-Modified 坑点:多台机器上同一个文件的修改时间可能不一致。
  2. ETag 坑点:默认情况下,Nginx 等服务器生成的 ETag 包含了 inode (文件索引节点),不同机器上的 inode 是不同的。这就导致即使文件内容完全一致,请求分发到不同机器时 ETag 也会不同,从而导致缓存失效。解决方案:在分布式系统中,通常会配置 Nginx 关闭 ETag 的 inode 计算,仅根据文件大小和修改时间生成,或者干脆统一由构建工具(而不是 Nginx 默认策略)去控制强缓存,规避协商缓存的短板。