浏览器缓存是 Web 性能优化中最基础也最关键的环节之一。合理利用缓存,可以将重复请求的响应时间从数百毫秒压缩到近乎为零,同时大幅降低服务器带宽压力。但要真正驾驭缓存,必须理解其背后的两套核心机制:强缓存与协商缓存。它们之间的博弈,决定了每一次资源请求是“直接拿本地副本”还是“先问服务器再决定”。
为什么需要浏览器缓存
在没有缓存的情况下,用户每次访问页面,浏览器都要向服务器请求所有 HTML、CSS、JavaScript、图片等资源。即使资源内容完全没有变化,网络往返的延迟和带宽消耗依然存在。缓存的核心目标就是:对于未发生变化的资源,避免重复传输。
浏览器缓存大致可分为两类:
- 强缓存:浏览器直接使用本地副本,不向服务器发送请求。
- 协商缓存:浏览器向服务器发送验证请求,由服务器判断本地副本是否仍然有效。
两者并非互斥,而是根据资源的新鲜程度和服务器策略交替生效。
强缓存:不闻不问,直接用
强缓存通过响应头中的 Expires 或 Cache-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 是较新的指令,告诉浏览器在缓存有效期内即使用户刷新页面也不要发起验证请求。
强缓存的命中与失效
强缓存是否命中,取决于两个条件:
- 本地存在该资源的缓存副本。
- 缓存副本仍在有效期内(根据
max-age或Expires计算)。
一旦过期,浏览器不会直接丢弃缓存,而是进入协商缓存阶段,向服务器发起验证请求。
协商缓存:先问再拿
协商缓存的核心思想是:缓存可能还有效,但需要服务器确认。浏览器携带上次响应中的验证标识向服务器发起请求,服务器根据标识判断资源是否变化:
- 未变化:返回
304 Not Modified,不携带响应体,浏览器继续使用本地副本。 - 已变化:返回
200 OK和新的资源内容,同时更新缓存标识。
协商缓存通过两对头部字段实现:Last-Modified / If-Modified-Since 和 ETag / 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-Match。ETag 精度更高,能解决秒级修改和修改时间失真的问题,但也会增加服务器计算开销(尤其是动态生成 ETag 时)。
强缓存与协商缓存的博弈
理解了两套机制后,关键问题在于:它们如何协同工作?
实际流程如下:
- 浏览器发起请求,首先检查强缓存。
- 若强缓存命中(未过期),直接使用本地副本,不发起网络请求。
- 若强缓存未命中(已过期或未设置),浏览器进入协商缓存流程,携带
If-None-Match或If-Modified-Since发起请求。 - 服务器校验后,返回
304(继续用缓存)或200(返回新资源)。 - 若响应中包含新的强缓存头,浏览器更新缓存策略,下次请求重新从第 1 步开始。
这场“博弈”的本质是时间与验证的权衡:
- 强缓存追求极致速度,但代价是缓存过期前无法感知资源变化。
- 协商缓存追求准确性,但每次都要付出一次网络往返的代价。
因此,最佳实践通常是组合使用:
- 对带哈希指纹的静态资源(如
app.a1b2c3.js),设置长期强缓存(max-age=31536000, immutable)。因为文件内容变化时文件名会变,不存在“过期但内容已变”的问题。 - 对HTML 入口文件,设置
no-cache或较短的max-age,配合协商缓存。确保用户总能获取最新的资源引用。 - 对API 响应,根据业务需求选择
no-store、no-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-Control、Expires、ETag、Last-Modified 四个头部字段的作用与优先级,掌握 304 响应的触发条件,才能在实际项目中制定合理的缓存策略。缓存不是简单的“开或关”,而是一套需要根据资源类型、更新频率和业务场景精细调控的体系。只有让强缓存和协商缓存各司其职、协同工作,才能在性能与正确性之间找到最佳平衡点。
未经允许不得转载:任鹏个人博客 » 浏览器缓存机制详解:强缓存与协商缓存的博弈


朋友圈点赞图在线生成源码