浏览器缓存机制详解:强缓存与协商缓存的博弈

浏览器缓存是 Web 性能优化中最基础也最关键的环节之一。合理利用缓存,可以将重复请求的响应时间从数百毫秒压缩到近乎为零,同时大幅降低服务器带宽压力。但要真正驾驭缓存,必须理解其背后的两套核心机制:强缓存协商缓存。它们之间的博弈,决定了每一次资源请求是“直接拿本地副本”还是“先问服务器再决定”。

为什么需要浏览器缓存

在没有缓存的情况下,用户每次访问页面,浏览器都要向服务器请求所有 HTML、CSS、JavaScript、图片等资源。即使资源内容完全没有变化,网络往返的延迟和带宽消耗依然存在。缓存的核心目标就是:对于未发生变化的资源,避免重复传输

浏览器缓存大致可分为两类:

  • 强缓存:浏览器直接使用本地副本,不向服务器发送请求。
  • 协商缓存:浏览器向服务器发送验证请求,由服务器判断本地副本是否仍然有效。

两者并非互斥,而是根据资源的新鲜程度和服务器策略交替生效。

强缓存:不闻不问,直接用

强缓存通过响应头中的 ExpiresCache-Control 来控制。当浏览器判断本地缓存仍然“新鲜”时,会直接使用缓存副本,不会产生任何网络请求。在 Chrome DevTools 的 Network 面板中,这类请求会显示为 200 (from disk cache)200 (from memory cache)

Expires:HTTP/1.0 的遗产

Expires 是 HTTP/1.0 引入的响应头,值为一个绝对时间戳,例如:

Expires: Wed, 21 Oct 2025 07:28:00 GMT

浏览器将该时间与本地时间比较,若未过期则使用缓存。问题在于:它依赖客户端时间。如果用户系统时间不准确,缓存策略就会失效。此外,服务器时间与客户端时间也可能存在偏差。

Cache-Control:HTTP/1.1 的标准方案

Cache-Control 是 HTTP/1.1 引入的响应头,优先级高于 Expires。它使用相对时间,不依赖客户端时钟。常见指令包括:

  • max-age=3600:缓存有效期为 3600 秒。
  • no-cache不是不缓存,而是每次使用缓存前必须向服务器验证(走协商缓存)。
  • no-store:完全不缓存,任何副本都不允许存储。
  • public / private:指定缓存是否可被代理服务器共享。
  • must-revalidate:缓存过期后必须向源服务器验证。

一个典型的强缓存响应头如下:

Cache-Control: max-age=31536000, immutable

immutable 是较新的指令,告诉浏览器在缓存有效期内即使用户刷新页面也不要发起验证请求。

强缓存的命中与失效

强缓存是否命中,取决于两个条件:

  1. 本地存在该资源的缓存副本。
  2. 缓存副本仍在有效期内(根据 max-ageExpires 计算)。

一旦过期,浏览器不会直接丢弃缓存,而是进入协商缓存阶段,向服务器发起验证请求。

协商缓存:先问再拿

协商缓存的核心思想是:缓存可能还有效,但需要服务器确认。浏览器携带上次响应中的验证标识向服务器发起请求,服务器根据标识判断资源是否变化:

  • 未变化:返回 304 Not Modified,不携带响应体,浏览器继续使用本地副本。
  • 已变化:返回 200 OK 和新的资源内容,同时更新缓存标识。

协商缓存通过两对头部字段实现:Last-Modified / If-Modified-SinceETag / If-None-Match

Last-Modified 与 If-Modified-Since

服务器在首次响应中返回:

Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

浏览器下次请求同一资源时,会携带:

If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

服务器比较该时间与资源当前最后修改时间。若资源未更新,返回 304;否则返回新资源。

这套机制的问题在于:

  • 精度只到秒级:如果资源在 1 秒内被多次修改,Last-Modified 无法感知。
  • 文件内容未变但修改时间变了:例如重新部署时文件被重新生成,Last-Modified 会更新,导致缓存失效。
  • 文件内容变了但修改时间没变:某些构建工具可能保留原始时间戳,导致缓存误判。

ETag 与 If-None-Match

ETag 是服务器为资源生成的唯一标识符,通常是内容哈希或版本号:

ETag: "5d8c72a5-264b"

浏览器下次请求时携带:

If-None-Match: "5d8c72a5-264b"

服务器比较 ETag,若一致则返回 304,否则返回新资源和新 ETag。

ETag 的优先级高于 Last-Modified。当两者同时存在时,服务器会优先校验 If-None-MatchETag 精度更高,能解决秒级修改和修改时间失真的问题,但也会增加服务器计算开销(尤其是动态生成 ETag 时)。

强缓存与协商缓存的博弈

理解了两套机制后,关键问题在于:它们如何协同工作?

实际流程如下:

  1. 浏览器发起请求,首先检查强缓存。
  2. 若强缓存命中(未过期),直接使用本地副本,不发起网络请求
  3. 若强缓存未命中(已过期或未设置),浏览器进入协商缓存流程,携带 If-None-MatchIf-Modified-Since 发起请求。
  4. 服务器校验后,返回 304(继续用缓存)或 200(返回新资源)。
  5. 若响应中包含新的强缓存头,浏览器更新缓存策略,下次请求重新从第 1 步开始。

这场“博弈”的本质是时间与验证的权衡

  • 强缓存追求极致速度,但代价是缓存过期前无法感知资源变化。
  • 协商缓存追求准确性,但每次都要付出一次网络往返的代价。

因此,最佳实践通常是组合使用

  • 带哈希指纹的静态资源(如 app.a1b2c3.js),设置长期强缓存(max-age=31536000, immutable)。因为文件内容变化时文件名会变,不存在“过期但内容已变”的问题。
  • HTML 入口文件,设置 no-cache 或较短的 max-age,配合协商缓存。确保用户总能获取最新的资源引用。
  • API 响应,根据业务需求选择 no-storeno-cache 或短 max-age

实战中的常见误区

误区一:no-cache 是不缓存。 实际上 no-cache 允许缓存,只是每次使用前必须验证。真正不缓存的是 no-store

误区二:设置了强缓存就万事大吉。 如果资源更新了但 URL 没变,用户会在 max-age 到期前一直使用旧版本。解决方案是使用内容哈希命名。

误区三:ETag 一定比 Last-Modified 好。 ETag 精度更高,但在分布式服务器环境中,不同节点生成的 ETag 可能不一致,导致缓存频繁失效。此时需要统一 ETag 生成策略或改用 Last-Modified。

误区四:刷新页面会跳过缓存。 普通刷新(F5)会跳过强缓存但保留协商缓存;强制刷新(Ctrl+Shift+R)才会同时跳过两者。

总结

浏览器缓存机制的核心在于强缓存与协商缓存的配合。强缓存负责“快”,协商缓存负责“准”。理解 Cache-ControlExpiresETagLast-Modified 四个头部字段的作用与优先级,掌握 304 响应的触发条件,才能在实际项目中制定合理的缓存策略。缓存不是简单的“开或关”,而是一套需要根据资源类型、更新频率和业务场景精细调控的体系。只有让强缓存和协商缓存各司其职、协同工作,才能在性能与正确性之间找到最佳平衡点。

未经允许不得转载:任鹏个人博客 » 浏览器缓存机制详解:强缓存与协商缓存的博弈

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏