彻底搞懂 HTTP 缓存:从强缓存到协商缓存
本文最后更新于:2026年2月11日 晚上
彻底搞懂 HTTP 缓存
为什么需要缓存
想象一下没缓存的世界:每次打开同一个网站,浏览器都要重新下载 CSS、JS、图片。浪费流量不说,页面还慢得离谱。缓存的价值其实就三点:
- 省带宽:重复资源不用重新下载
- 省时间:本地读取比网络请求快太多了
- 省服务器资源:减轻源站压力
缓存是 Web 性能优化里最简单、效果也最明显的手段。最直观的感受就是页面变快了。
缓存的工作流程
第一次请求资源时,服务器会在响应头里埋下一些信息,通过 Cache-Control、Expires 这些字段告诉浏览器:这个资源你可以缓存多久,下次来要不要问我。
整个流程大概是这样:
强缓存阶段:浏览器先看本地有没有这个资源的缓存,如果在有效期内,直接拿来用,连请求都不会发。这时候状态码显示 200 (from memory cache) 或 200 (from disk cache)。内存缓存比磁盘缓存快,但容量有限,页面关闭就没了;磁盘缓存容量大,能存很久。
协商缓存阶段:如果强缓存过期了,浏览器不会直接重新下载,而是带上”令牌”(ETag 或 Last-Modified)去问服务器:”我这东西还能用吗?”服务器对比后发现资源没变,就回一个 304 Not Modified,告诉浏览器继续用缓存。虽然发了请求,但响应体是空的,省了不少流量。
重新加载:如果服务器发现资源变了,或者根本没缓存,就返回 200 和新内容,浏览器更新本地缓存。
大致流程图:
1 | |
强缓存(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 是服务器给文件生成的”指纹”,通常是文件内容的哈希值。内容只要有任何变化,指纹就会完全不同。
工作流程:
- 第一次请求,服务器返回资源时带上
ETag: "33a64df5" - 浏览器把 ETag 存起来
- 缓存过期后,浏览器发起协商请求,请求头带上
If-None-Match: "33a64df5" - 服务器计算当前文件的 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 | |
HTML 是入口文件,经常变化。让它每次都用协商缓存验证,确保用户拿到最新版本。
场景 2:带指纹的静态资源(JS/CSS/图片)
1 | |
文件名里带了内容哈希(比如 app.a3f2b1c.js),内容变了文件名就变,可以缓存一年。immutable 告诉浏览器这期间连协商缓存请求都不用发。
场景 3:API 接口
1 | |
动态数据不缓存,每次都拿最新的。
场景 4:用户信息等半静态数据
1 | |
可以缓存,但只能浏览器缓存,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 更可靠