Lazy loaded image
前端
HTTP 缓存详解:强缓存与协商缓存 (Expires, Cache-Control, ETag, Last-Modified)
字数 3763阅读时长 10 分钟
2024-7-12
2025-4-29
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 响应头:ExpiresCache-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: 强制进行协商缓存。浏览器在使用缓存前,必须向服务器发送请求(携带缓存标识如 ETagLast-Modified),验证资源是否未变 (304 Not Modified)。注意:不是完全不缓存,而是每次都需验证。
      • no-store: 完全禁止缓存。浏览器不存储响应的任何副本。
      • public: 响应可以被任何中间缓存(如 CDN、代理服务器)缓存。
      • private: 响应只能被用户的浏览器缓存,不允许中间缓存。
    • 优先级:Cache-ControlExpires 同时存在时,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
    运行与测试:
    1. 准备两张不同的图片,命名为 image1.jpgimage2.jpg
    1. 在项目根目录运行 node index.js 启动服务器。
    1. 用浏览器访问 http://localhost:8080/
    1. 打开开发者工具 (F12),切换到 Network (网络) 面板。
    观察结果:
    • 首次请求:
      • index.htmlimage1.jpg 的状态码都是 200 OK
      • 查看 image1.jpg 的响应头 (Response Headers),可以看到 Cache-Control: max-age=86400
      • Size 列显示了资源的实际大小。
    • 刷新页面 (普通刷新 F5):
      • index.html 可能会重新请求(取决于浏览器对导航请求的处理),状态码可能是 200 OK304 Not Modified (如果服务器也配置了协商缓存)。
      • image1.jpg 的状态码很可能是 200 OK,但 Size 列会显示 (memory cache)(disk cache),Time 列接近 0ms。这表示浏览器直接从本地缓存加载了图片,没有发起网络请求。
      • 此时查看 image1.jpg 的请求头,可能会看到 Provisional headers are shown 的提示,因为实际的网络请求未发生。
    • 验证缓存有效性:
        1. 停止 Node.js 服务器。
        1. 删除项目中的 image1.jpg 文件。
        1. image2.jpg 重命名为 image1.jpg
        1. 重新启动 Node.js 服务器 (node index.js)。
        1. 再次普通刷新 (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-SinceETag / If-None-Match

    2.1 Last-Modified / If-Modified-Since

    • 工作流程:
        1. 首次请求: 服务器在响应头中包含 Last-Modified 字段,值为资源的最后修改时间 (GMT 格式)。
        1. 后续请求 (强缓存失效后): 浏览器在请求头中包含 If-Modified-Since 字段,其值为上次响应中的 Last-Modified 值。
        1. 服务器判断: 服务器比较 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 更精确、更可靠。
    • 工作流程:
        1. 首次请求: 服务器在响应头中包含 ETag 字段,值为资源的唯一标识,例如 ETag: "a1b2c3d4e5" (ETag 值通常用双引号包围)。
        1. 后续请求 (强缓存失效后): 浏览器在请求头中包含 If-None-Match 字段,其值为上次响应中的 ETag 值。
        1. 服务器判断: 服务器比较 If-None-Match 的值与资源的当前 ETag 值。
            • 如果值一致,表示资源未修改,返回 304 Not Modified
            • 如果值不一致,表示资源已修改,返回 200 OK、新资源和新的 ETag 值。
    • 设置 (Node.js 示例):
    • 优先级:ETag / If-None-MatchLast-Modified / If-Modified-Since 同时存在时,服务器会优先使用 ETag 进行比较,因为它更精确。

    实践:测试协商缓存

    使用上述配置了协商缓存 (无论是 Last-Modified 还是 ETag) 的 Node.js 代码运行服务器。
    观察结果:

    总结:缓存策略的选择

    • 强缓存优先: 浏览器总是先检查强缓存 (Cache-Control / Expires)。如果命中且有效,直接使用缓存,不访问服务器。
    • 协商缓存备用: 如果强缓存未命中或失效,浏览器才发起协商缓存请求(带 If-Modified-Since / If-None-Match)。
    • Cache-Control vs Expires: 优先使用 Cache-Controlmax-age,因为它更精确且不受客户端时间影响。
    • ETag vs Last-Modified: 优先使用 ETag,因为它能更准确地反映资源内容的变化。两者可以同时使用,服务器会优先验证 ETag
    最佳实践建议:
    • 不常变动的资源 (如库文件、字体、旧图片): 使用较长的 max-age 强缓存,配合基于文件内容的 ETag 或文件名哈希 (cache busting) 实现更新。
    • 经常变动的资源 (如 HTML 文件、API 数据): 使用 Cache-Control: no-cache 强制进行协商缓存,或使用很短的 max-age (如 max-age=0),确保用户能及时获取更新,同时利用 ETagLast-Modified 在未更新时返回 304 节省带宽。
    • 敏感数据或完全不应缓存的内容: 使用 Cache-Control: no-store
    理解并合理配置 HTTP 缓存策略,对于提升网站性能和用户体验至关重要。
    上一篇
    AJAX 轮询 vs. WebSocket 推送:实时 Web 通信方案对比与 JavaScript 实现
    下一篇
    Vite Plugin 优化 svg,就这么简单搞定!