HTTP 缓存策略:强缓存、协商缓存与 CDN
HTTP 缓存策略核心概念
HTTP 缓存是 Web 性能优化中最关键的技术之一。它允许浏览器或中间代理服务器存储资源的副本,在后续请求时直接使用副本而无需从源服务器重新下载。这不仅大幅降低了服务器负载,还显著减少了页面加载时间,改善了用户体验。
从策略层面,HTTP 缓存主要分为两大类:强缓存和协商缓存。理解它们的区别与协作方式,是掌握缓存优化的基础。
强缓存:无需询问服务器
强缓存是指浏览器直接使用本地缓存的资源,不会向服务器发送任何请求。只要缓存未过期,资源就从本地读取,状态码表现为 200 (from memory cache) 或 200 (from disk cache)。
控制强缓存的核心 HTTP 响应头有两种:
-
Expires:HTTP/1.0 时代的产物,值为一个绝对时间的 GMT 格式字符串。例如Expires: Thu, 01 Dec 2025 16:00:00 GMT,告诉浏览器在这个时间点之前可以直接使用缓存。其缺点明显:依赖客户端本地时间,如果客户端时间不准确,缓存判断就会出错。 -
Cache-Control:HTTP/1.1 引入的现代化缓存控制头,优先级高于Expires。它使用相对时间或指令,功能更强大、更灵活。常用指令包括:max-age=<seconds>:资源被视为新鲜的最大时长,单位为秒。例如Cache-Control: max-age=31536000表示资源在一年内都有效。这是最常用的强缓存指令。s-maxage=<seconds>:仅用于共享缓存(如 CDN),覆盖max-age对共享缓存的作用。public:表明响应可以被任何对象(浏览器、CDN 等)缓存。private:表明响应只能被浏览器缓存,不允许中间代理缓存。no-cache:不是不缓存,而是要求在使用缓存前必须向服务器进行新鲜度验证(即必须走协商缓存)。no-store:真正禁止缓存,每次都必须从服务器获取完整响应。must-revalidate:缓存过期后,必须重新验证才能使用,不能直接使用过期的缓存。
强缓存验证流程:
- 浏览器首次请求资源,服务器返回
Cache-Control: max-age=3600。 - 一小时内再次请求该资源,浏览器直接检查缓存,未过期则直接从缓存中取出资源,网络面板显示
200 (disk cache)。 - 一小时后请求,缓存过期,进入协商缓存阶段。
协商缓存:服务器验证新鲜度
当强缓存失效,或用户明确刷新页面(如按 F5),浏览器会携带缓存标识向服务器发送条件请求。服务器根据标识判断资源是否变化:
- 如果未变化,返回
304 Not Modified,浏览器继续使用本地缓存,此时只传输了很短的响应头,无响应体,节省带宽。 - 如果已变化,返回
200及新的资源内容和新的缓存头。
协商缓存依赖两对请求/响应头:
基于最后修改时间:Last-Modified / If-Modified-Since
- 服务器在首次响应中返回
Last-Modified头,表示资源的最后修改时间。 - 浏览器缓存此时间,下次请求时通过
If-Modified-Since头将该时间发送给服务器。 - 服务器对比资源的最后修改时间:
- 如果一致,返回
304,浏览器使用缓存。 - 如果不一致,返回
200并带上新的资源和新的Last-Modified。
- 如果一致,返回
这种方式有两个缺陷:
- 时间精度通常为秒级,如果资源在 1 秒内多次修改,缓存可能无法及时更新。
- 某些场景下文件被定期生成,内容实际未变但
Last-Modified变了,导致无效的重新下载。
基于内容标识:ETag / If-None-Match
ETag(Entity Tag)是资源的唯一标识,通常是文件内容的哈希值或版本号。它解决了 Last-Modified 的精度问题,是更可靠的验证方式。
- 服务器返回
ETag: "abc123"。 - 浏览器保存该值,下次请求通过
If-None-Match: "abc123"发送给服务器。 - 服务器比对
ETag:- 相同则返回
304。 - 不同则返回
200及新内容与新的ETag。
- 相同则返回
ETag 优先级高于 Last-Modified。当两者同时存在时,服务器会先检查 If-None-Match,验证通过后不会再去检查 If-Modified-Since。
协商缓存请求流程示意(强制刷新或 max-age 过期后):
- 浏览器请求资源,自动携带
If-None-Match和/或If-Modified-Since。 - 服务器根据
ETag或Last-Modified判断资源未变,返回304 Not Modified。 - 浏览器收到
304,从缓存中读取资源并更新缓存有效期(根据返回的新鲜缓存头)。
强缓存与协商缓存的协作
一个完整的缓存决策流程如下:
- 浏览器发起请求。
- 检查强缓存:如果
Cache-Control: max-age未过期或Expires时间未到,则直接使用缓存,不发送请求。 - 强缓存失效:发送请求,自动带上协商缓存的验证条件(
If-None-Match或If-Modified-Since)。 - 服务器验证:
- 资源未变 → 返回
304,浏览器继续使用缓存。 - 资源已变 → 返回
200和新资源,更新本地缓存和缓存头。
- 资源未变 → 返回
在实际项目中,常使用 Cache-Control: max-age=0, must-revalidate 或 Cache-Control: no-cache 来直接进入协商缓存,这常用于 HTML 主文档的缓存策略——每次请求都验证,但未修改时只返回 304,既保证实时性又节省带宽。
CDN 与 HTTP 缓存
CDN(内容分发网络)是广泛部署在全球各地的缓存服务器网络。它将源站内容分发到距离用户更近的边缘节点,通过 HTTP 缓存头控制 CDN 节点的缓存行为。
CDN 缓存的工作方式
CDN 节点本质上是一个巨大的“共享缓存”。当用户请求某个资源时:
- 如果 CDN 节点已有该资源的有效缓存,则直接返回给用户,无需回源。
- 如果缓存过期或不存在,CDN 向源站发起请求,获取资源后存储在节点上,再返回给用户。
CDN 缓存同样遵循 HTTP 缓存头,但有一些特殊之处:
s-maxage可以单独控制 CDN 的缓存时长,而不影响浏览器缓存。public明确告知 CDN 该资源可缓存,private则告知不可缓存。no-cache在 CDN 中的行为可能被重写,有些 CDN 提供配置项将no-cache作为需要验证的指令遵守,而有些则会直接理解为不缓存。
CDN 缓存层级与刷新
CDN 通常有层级缓存架构,例如 L1(边缘节点)和 L2(父节点)。大多数请求会被边缘节点直接响应,边缘节点未命中时逐级向上请求,最终回源。这种机制大大减轻了源站压力。
CDN 缓存刷新是运维的重要操作:
- 目录刷新:更新某个目录下所有资源的缓存(例如网站改版后刷新
/static/下所有文件)。 - URL 刷新:使单个资源的缓存立即失效。
- 预热:主动将静态资源推送到 CDN 节点,这样用户首次访问时就能命中缓存。
结合强缓存与 CDN 的最佳实践
对于不常变化的静态资源(CSS、JS、图片、字体),推荐采用文件名指纹(hash)加长期强缓存结合 CDN 的策略:
- 构建工具为每个文件生成唯一的哈希值,如
main.1a2b3c.js。 - 服务器设置极大的
Cache-Control: max-age=31536000, public(一年),并配置 CDN 也遵守该头。 - 当文件内容更新时,文件名随之改变,相当于发布了一个新 URL。旧的 URL 仍然被缓存,新 URL 强制获取新内容,这样既能享受极致的缓存,又不存在更新不及时的问题。
对于 HTML 主文档,通常设置 Cache-Control: no-cache 并让 CDN 回源验证,保证用户总能获取最新页面结构。
缓存策略速查表
| 场景 | Cache-Control 头示例 | 说明 |
|---|---|---|
| 长期不变静态资源(带指纹) | public, max-age=31536000, immutable |
一年强缓存,immutable 表示资源永不变,浏览器无需重验证 |
| 可能变化的静态资源 | max-age=604800 |
一周强缓存,过期后协商缓存 |
| HTML 文档 | no-cache 或 max-age=0, must-revalidate |
每次验证,保证实时性,但仍可借助304节省传输 |
| 敏感数据(用户私有) | private, no-cache, no-store |
仅浏览器缓存,每次验证,部分场景需完全禁止存储 |
| CDN 缓存但浏览器不缓存 | s-maxage=3600, max-age=0 |
CDN 缓存一小时,浏览器每次需验证 |
调试与验证缓存行为
实际开发中,可通过以下方式确认缓存是否生效:
- 浏览器开发者工具 (DevTools):
- Network 面板的
Size列显示(disk cache)或(memory cache)表示来自强缓存。 - 状态码
304表示协商缓存命中。 - 查看请求的
Request Headers确认是否携带If-None-Match等。
- Network 面板的
- cURL 命令:
返回的响应头包含curl -I https://example.com/style.cssCache-Control、ETag、Last-Modified等信息。 - CDN 提供商的缓存命中率面板:查看命中和回源次数,判断配置是否合理。
常见误区与注意事项
- 认为
no-cache是不缓存:no-cache意味着每次使用前必须验证,资源本身仍可被缓存;真正禁止缓存的是no-store。 - 同时设置强缓存和
no-cache导致混淆:Cache-Control: max-age=3600, no-cache虽然不常见,但规范中no-cache会覆盖max-age,导致每次都验证。应避免无意义组合。 - 只依赖
ETag而忽略Last-Modified:在某些 CDN 或旧系统中,ETag可能被剥离或计算方式导致不一致,保留Last-Modified作为兜底是可靠的选择。 - 刷新按钮与强缓存的关系:普通刷新 (F5) 会忽略强缓存直接带条件请求;硬刷新 (Ctrl+F5) 则会加上
Cache-Control: no-cache和Pragma: no-cache请求头,强制跳过所有缓存。 - 缓存静态资源但忘记更新文件名:这是性能优化的经典陷阱,导致用户浏览器长时间使用旧版本文件,必须配合文件指纹策略。
总结
合理的 HTTP 缓存策略是构建高性能 Web 应用的基石。通过结合强缓存与协商缓存,并利用 CDN 的分布式缓存能力,可以极大提升资源加载速度。核心原则是:为不变的资源分配唯一的 URL 和长久的缓存时间,为变化的资源配置即时验证机制。 掌握这套方法论,你就能在绝大多数场景下设计出高效、可靠的缓存方案。