彻底搞懂 HTTP 缓存:从强缓存到协商缓存

本文最后更新于:2026年2月11日 晚上

彻底搞懂 HTTP 缓存

为什么需要缓存

想象一下没缓存的世界:每次打开同一个网站,浏览器都要重新下载 CSS、JS、图片。浪费流量不说,页面还慢得离谱。缓存的价值其实就三点:

  • 省带宽:重复资源不用重新下载
  • 省时间:本地读取比网络请求快太多了
  • 省服务器资源:减轻源站压力

缓存是 Web 性能优化里最简单、效果也最明显的手段。最直观的感受就是页面变快了。

缓存的工作流程

第一次请求资源时,服务器会在响应头里埋下一些信息,通过 Cache-ControlExpires 这些字段告诉浏览器:这个资源你可以缓存多久,下次来要不要问我。

整个流程大概是这样:

  1. 强缓存阶段:浏览器先看本地有没有这个资源的缓存,如果在有效期内,直接拿来用,连请求都不会发。这时候状态码显示 200 (from memory cache) 或 200 (from disk cache)。内存缓存比磁盘缓存快,但容量有限,页面关闭就没了;磁盘缓存容量大,能存很久。

  2. 协商缓存阶段:如果强缓存过期了,浏览器不会直接重新下载,而是带上”令牌”(ETag 或 Last-Modified)去问服务器:”我这东西还能用吗?”服务器对比后发现资源没变,就回一个 304 Not Modified,告诉浏览器继续用缓存。虽然发了请求,但响应体是空的,省了不少流量。

  3. 重新加载:如果服务器发现资源变了,或者根本没缓存,就返回 200 和新内容,浏览器更新本地缓存。

大致流程图:

1
2
3
4
5
6
7
请求资源 → 本地有缓存?
├─ 没有 → 请求服务器 → 返回 200 → 存入缓存
└─ 有 → 在有效期内?
├─ 是 → 直接用缓存 (200 from cache)
└─ 否 → 发协商缓存请求 (带 ETag/Last-Modified)
├─ 服务器说"没变" → 返回 304 → 刷新缓存时间
└─ 服务器说"变了" → 返回 200 + 新内容 → 更新缓存

强缓存(Strong Cache)

强缓存是最理想的缓存状态——浏览器压根不发请求,直接从本地读取,速度快得飞起。

核心 Header 字段

1. Cache-Control (HTTP/1.1)

这是目前最主流的控制方式。它是相对时间,从请求成功的那一刻开始倒计时。

常用指令:

  • max-age=3600:资源在 3600 秒(1小时)内有效
  • public:客户端和中间代理(CDN)都可以缓存
  • private:只有浏览器可以缓存,代理不能缓存(适合个性化内容)
  • no-cache:听起来是禁用缓存,其实是跳过强缓存,直接走协商缓存验证
  • no-store:真正的禁用缓存,每次都重新下载(适合敏感数据)
  • immutable:有效期内哪怕用户强制刷新,也不去问服务器(Facebook 等大厂常用,配合文件指纹使用)

2. Expires (HTTP/1.0)

这是服务器返回的一个绝对时间点(格林威治时间),比如 Expires: Wed, 21 Oct 2025 07:28:00 GMT

它的致命缺点是依赖客户端本地时间。如果用户手动把电脑时间调到 2030 年,缓存可能永远不过期;调到 2000 年,缓存又可能永远过期。所以现在基本被 Cache-Control 替代了。如果两者同时存在,Cache-Control 优先级更高。

3. Pragma (HTTP/1.0)

只有一个值 no-cache,作用和 Cache-Control: no-cache 类似。存在主要是为了兼容老系统,现代开发基本用不到。


协商缓存(Comparison Cache)

强缓存过期了,不代表缓存就没用了。这时候进入协商缓存阶段——浏览器带上”凭证”去问服务器:”我这东西还能继续用吗?”

协商缓存的核心是:如果资源没变,服务器只返回一个 304 状态码,响应体是空的。浏览器收到 304,就知道可以继续用本地缓存,同时刷新缓存的有效期。

核心 Header 字段

1. ETag / If-None-Match(推荐)

ETag 是服务器给文件生成的”指纹”,通常是文件内容的哈希值。内容只要有任何变化,指纹就会完全不同。

工作流程:

  1. 第一次请求,服务器返回资源时带上 ETag: "33a64df5"
  2. 浏览器把 ETag 存起来
  3. 缓存过期后,浏览器发起协商请求,请求头带上 If-None-Match: "33a64df5"
  4. 服务器计算当前文件的 ETag,如果和请求中的一致,返回 304;如果不一致,返回 200 和新资源

优点

  • 精确度高,只要文件内容不变,ETag 就不变
  • 能识别出”内容没变但修改时间变了”的情况(比如重新打包但代码没改)

缺点

  • 服务器需要计算哈希,有一定开销
  • 如果服务器是集群部署,不同节点生成的 ETag 可能不同(可以用 W/“weak” 前缀解决)

2. Last-Modified / If-Modified-Since

这是服务器返回的文件最后修改时间,精确到秒。

工作流程和 ETag 类似,只是比对的是时间戳。但有几个坑:

  • 精度问题:只能精确到秒,如果一秒内改了多次,它感知不到
  • 可靠性问题:文件内容没变,但 touch 一下修改时间变了,会导致缓存失效;反过来,内容变了但修改时间没变(比如用工具刻意保持时间戳),缓存就不会更新

优先级:如果 ETag 和 Last-Modified 同时存在,ETag 优先。因为 ETag 更可靠。


刷新操作对缓存的影响

浏览器不同的刷新方式,对缓存的影响完全不同:

  • **普通刷新 (F5 / Cmd+R)**:强制跳过强缓存,但会带协商缓存请求。开发时常用这个来验证资源是否更新。
  • **强制刷新 (Ctrl+F5 / Cmd+Shift+R)**:所有缓存都不要,直接请求最新资源(返回 200)。相当于在请求头里加了 Cache-Control: no-cache
  • 地址栏回车:按正常流程,先检查强缓存。如果缓存还在有效期内,直接读缓存。
  • 前进/后退按钮:很多浏览器会优先使用缓存,即使已经过期,以提升用户体验。

这个小细节在调试时很重要。有时候你改了代码刷新页面却没生效,可能就是刷新方式不对。

缓存的实际配置

看一堆概念可能有点晕,直接看几个实际场景的配置:

场景 1:HTML 文件

1
Cache-Control: no-cache

HTML 是入口文件,经常变化。让它每次都用协商缓存验证,确保用户拿到最新版本。

场景 2:带指纹的静态资源(JS/CSS/图片)

1
Cache-Control: public, max-age=31536000, immutable

文件名里带了内容哈希(比如 app.a3f2b1c.js),内容变了文件名就变,可以缓存一年。immutable 告诉浏览器这期间连协商缓存请求都不用发。

场景 3:API 接口

1
Cache-Control: no-store

动态数据不缓存,每次都拿最新的。

场景 4:用户信息等半静态数据

1
Cache-Control: private, max-age=3600

可以缓存,但只能浏览器缓存,CDN 不缓存。适合用户个性化的内容。

总结

维度 强缓存 协商缓存
是否发请求 不发 发(但响应体可能为空)
状态码 200 (from cache) 304
核心字段 Cache-Control/Expires ETag/Last-Modified
速度 极快 较快

实际工作中的最佳实践:

  • HTML 设置 no-cache,确保每次都能加载最新版本
  • 静态资源文件名加 hash(如 main.a1b2c3.js),设置超长 max-age
  • API 接口根据业务场景决定是否缓存,敏感数据用 no-store
  • 优先考虑 ETag,因为它比 Last-Modified 更可靠

彻底搞懂 HTTP 缓存:从强缓存到协商缓存
http://bestkele.com/2021/07/22/concept/cache/
作者
kele
发布于
2021年7月22日
许可协议