type
status
date
slug
summary
tags
category
icon
password
HTTP 缓存详解:强缓存与协商缓存 (
Expires, Cache-Control, ETag, Last-Modified)Meta Description: 深入理解 HTTP 缓存机制,学习强缓存(Expires, Cache-Control)和协商缓存(ETag, Last-Modified)的工作原理与配置。通过 Node.js 实例掌握提升网站性能的关键技术。
(文章正文)
引言:为什么需要 HTTP 缓存?
HTTP 缓存是 Web 性能优化的关键技术之一,旨在减少网络带宽消耗、降低服务器负载并加快页面加载速度。当用户首次访问网页时,浏览器会下载各种资源(HTML、CSS、JavaScript、图片等)。如果没有缓存,每次访问都需要重新下载这些资源,既耗时又浪费带宽。HTTP 缓存机制允许浏览器将这些资源的副本存储在本地,并在后续请求中根据一定的规则复用这些副本,从而显著提升用户体验。
HTTP 缓存主要分为两大类:强缓存 (Strong Cache) 和 协商缓存 (Negotiation Cache / Conditional Cache)。
1. 强缓存:本地副本的直接复用
强缓存策略指示浏览器在缓存有效期内直接使用本地缓存副本,无需向服务器发送任何请求。这提供了最快的资源加载速度。
实现强缓存主要依赖两个 HTTP 响应头:
Expires 和 Cache-Control。1.1 Expires (HTTP/1.0)
- 作用:
Expires响应头包含一个绝对的过期日期/时间 (GMT 格式),例如Expires: Wed, 21 Oct 2025 07:28:00 GMT。浏览器接收到带有此响应头的资源后,会将其缓存。在下次请求相同资源时,浏览器会比较当前时间和Expires指定的时间。如果当前时间早于Expires时间,则直接使用缓存,不发请求。
- 设置 (Nginx 示例):
- 缺点:
Expires依赖于客户端本地时间。如果客户端时间与服务器时间不一致(例如用户手动修改了本地时间),缓存的判断可能会出错,导致缓存提前失效或超期使用。因此,在 HTTP/1.1 中,Cache-Control被引入作为更可靠的替代方案。
1.2 Cache-Control (HTTP/1.1) - 推荐使用
- 作用:
Cache-Control提供了更灵活、更强大的缓存控制能力。最常用的指令是max-age=<seconds>,它指定了一个相对的缓存有效时长(从响应生成时开始计算,单位为秒),例如Cache-Control: max-age=3600表示缓存 1 小时。浏览器在max-age指定的时间内会直接使用缓存。
- 常用指令:
max-age=<seconds>: 缓存有效时长。no-cache: 强制进行协商缓存。浏览器在使用缓存前,必须向服务器发送请求(携带缓存标识如ETag或Last-Modified),验证资源是否未变 (304 Not Modified)。注意:不是完全不缓存,而是每次都需验证。no-store: 完全禁止缓存。浏览器不存储响应的任何副本。public: 响应可以被任何中间缓存(如 CDN、代理服务器)缓存。private: 响应只能被用户的浏览器缓存,不允许中间缓存。
- 优先级: 当
Cache-Control和Expires同时存在时,Cache-Control的优先级更高。
- 用户行为影响:
- 普通刷新 (F5): 通常会遵循
Cache-Control规则(如果max-age未过期,可能仍使用强缓存)。 - 强制刷新 (Ctrl+F5 / Cmd+Shift+R): 浏览器会在请求头中添加
Cache-Control: no-cache(或Pragma: no-cache),强制跳过强缓存,进行协商缓存。 - 地址栏回车 / 书签访问: 行为可能因浏览器而异,有时浏览器会添加
Cache-Control: no-cache,表现类似强制刷新,导致强缓存“失效”(实际上是浏览器主动请求验证)。
实践:使用 Cache-Control: max-age
我们用 Node.js 创建一个简单服务器来演示
max-age。项目结构:
index.js (Node.js Server)index.html运行与测试:
- 准备两张不同的图片,命名为
image1.jpg和image2.jpg。
- 在项目根目录运行
node index.js启动服务器。
- 用浏览器访问
http://localhost:8080/。
- 打开开发者工具 (F12),切换到 Network (网络) 面板。
观察结果:
- 首次请求:
index.html和image1.jpg的状态码都是200 OK。- 查看
image1.jpg的响应头 (Response Headers),可以看到Cache-Control: max-age=86400。 - Size 列显示了资源的实际大小。
- 刷新页面 (普通刷新 F5):
index.html可能会重新请求(取决于浏览器对导航请求的处理),状态码可能是200 OK或304 Not Modified(如果服务器也配置了协商缓存)。image1.jpg的状态码很可能是200 OK,但 Size 列会显示(memory cache)或(disk cache),Time 列接近 0ms。这表示浏览器直接从本地缓存加载了图片,没有发起网络请求。- 此时查看
image1.jpg的请求头,可能会看到Provisional headers are shown的提示,因为实际的网络请求未发生。
- 验证缓存有效性:
- 停止 Node.js 服务器。
- 删除项目中的
image1.jpg文件。 - 将
image2.jpg重命名为image1.jpg。 - 重新启动 Node.js 服务器 (
node index.js)。 - 再次普通刷新 (F5) 浏览器页面。你会发现页面上显示的仍然是旧的
image1.jpg(来自缓存),而不是你替换后的新图片。
- 强制刷新 (Ctrl+F5):
- 现在进行强制刷新。浏览器会忽略强缓存,向服务器发送请求。
image1.jpg的状态码变为200 OK,Size 显示实际大小,页面上会显示你替换后的新图片。- 请求头 (Request Headers) 中可以看到
Cache-Control: no-cache。
2. 协商缓存:与服务器确认资源有效性
当强缓存失效(过期或被用户操作跳过)时,浏览器会启用协商缓存。浏览器会向服务器发送一个条件请求 (Conditional Request),携带上次缓存资源时服务器返回的缓存标识。服务器根据这些标识判断资源是否有更新:
- 资源未更新: 服务器返回
304 Not Modified状态码,响应体为空。浏览器直接使用本地缓存的副本。这节省了传输响应体的带宽,但仍有一次 HTTP 请求/响应的开销。
- 资源已更新: 服务器返回
200 OK状态码,以及新的资源内容和新的缓存标识。浏览器使用新资源并更新本地缓存。
协商缓存主要依赖两组 HTTP 头:
Last-Modified / If-Modified-Since 和 ETag / If-None-Match。2.1 Last-Modified / If-Modified-Since
- 工作流程:
- 首次请求: 服务器在响应头中包含
Last-Modified字段,值为资源的最后修改时间 (GMT 格式)。 - 后续请求 (强缓存失效后): 浏览器在请求头中包含
If-Modified-Since字段,其值为上次响应中的Last-Modified值。 - 服务器判断: 服务器比较
If-Modified-Since的值与资源的当前最后修改时间。 - 如果时间一致,表示资源未修改,返回
304 Not Modified。 - 如果时间不一致,表示资源已修改,返回
200 OK、新资源和新的Last-Modified值。
- 设置 (Node.js 示例):
- 缺点:
- 时间精度问题: 文件最后修改时间只能精确到秒。如果文件在 1 秒内被多次修改,
Last-Modified不会变化,可能导致浏览器误认为资源未更新。 - 内容未变但时间变了: 有时文件内容没变,但最后修改时间却变了(例如文件被 touch 或重新保存),这会导致不必要的资源重新下载。
- 分布式服务器: 不同服务器上的文件最后修改时间可能不一致。
2.2 ETag / If-None-Match - 推荐使用
- 作用:
ETag(Entity Tag) 是服务器为资源生成的唯一标识符(通常是文件内容的哈希值或版本号)。只要资源内容改变,ETag就会改变。这比基于时间的Last-Modified更精确、更可靠。
- 工作流程:
- 首次请求: 服务器在响应头中包含
ETag字段,值为资源的唯一标识,例如ETag: "a1b2c3d4e5"(ETag 值通常用双引号包围)。 - 后续请求 (强缓存失效后): 浏览器在请求头中包含
If-None-Match字段,其值为上次响应中的ETag值。 - 服务器判断: 服务器比较
If-None-Match的值与资源的当前ETag值。 - 如果值一致,表示资源未修改,返回
304 Not Modified。 - 如果值不一致,表示资源已修改,返回
200 OK、新资源和新的ETag值。
- 设置 (Node.js 示例):
- 优先级: 当
ETag/If-None-Match和Last-Modified/If-Modified-Since同时存在时,服务器会优先使用ETag进行比较,因为它更精确。
实践:测试协商缓存
使用上述配置了协商缓存 (无论是
Last-Modified 还是 ETag) 的 Node.js 代码运行服务器。观察结果:
- 首次请求:
- 状态码
200 OK。 - 响应头包含
Last-Modified或ETag(或两者都有)。 <img src="https://mmbiz.qpic.cn/sz_mmbiz_jpg/IlE1Y2rl1uZiaicXaMXRX62V7hsnA4XcFGGQdvhYdoRAtZC1Q68rkJplP1uTpR71H3u2y7sWaJRibHg0Ua0Tic5DDQ/640?wx_fmt=other&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="协商缓存首次请求,状态码 200,包含 Last-Modified/ETag">
- 刷新页面 (普通刷新 F5):
- 状态码变为
304 Not Modified。 - 请求头包含
If-Modified-Since或If-None-Match。 - 响应体为空 (Size 很小)。 <img src="https://mmbiz.qpic.cn/sz_mmbiz_jpg/IlE1Y2rl1uZiaicXaMXRX62V7hsnA4XcFGDB4JAuiatwQLBnS5niabE5nLda9E7KAZqXU8a2MH9iaaK2LtmAyQ1HoJQ/640?wx_fmt=other&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="协商缓存刷新,状态码 304,资源未修改">
- 修改文件后刷新:
- 修改
index.html文件内容(例如,改变<h1>文本)。 - 刷新页面 (普通刷新 F5)。
index.html的状态码变回200 OK。- 响应头中的
Last-Modified或ETag值会更新。 - 页面显示修改后的内容。 <img src="https://mmbiz.qpic.cn/sz_mmbiz_jpg/IlE1Y2rl1uZiaicXaMXRX62V7hsnA4XcFGE5wFVlqePJC82Y9VcamibrykuyRY7mUOo9EmzXiayzIgBbmAqsETzRpA/640?wx_fmt=other&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="修改文件后刷新,状态码 200,协商缓存发现资源已更新">
总结:缓存策略的选择
- 强缓存优先: 浏览器总是先检查强缓存 (
Cache-Control/Expires)。如果命中且有效,直接使用缓存,不访问服务器。
- 协商缓存备用: 如果强缓存未命中或失效,浏览器才发起协商缓存请求(带
If-Modified-Since/If-None-Match)。
Cache-ControlvsExpires: 优先使用Cache-Control的max-age,因为它更精确且不受客户端时间影响。
ETagvsLast-Modified: 优先使用ETag,因为它能更准确地反映资源内容的变化。两者可以同时使用,服务器会优先验证ETag。
最佳实践建议:
- 不常变动的资源 (如库文件、字体、旧图片): 使用较长的
max-age强缓存,配合基于文件内容的ETag或文件名哈希 (cache busting) 实现更新。
- 经常变动的资源 (如 HTML 文件、API 数据): 使用
Cache-Control: no-cache强制进行协商缓存,或使用很短的max-age(如max-age=0),确保用户能及时获取更新,同时利用ETag或Last-Modified在未更新时返回304节省带宽。
- 敏感数据或完全不应缓存的内容: 使用
Cache-Control: no-store。
理解并合理配置 HTTP 缓存策略,对于提升网站性能和用户体验至关重要。
- 作者:90_blog
- 链接:https://blog.tri7e.com/article/js_huancun
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
