HTTP 缓存怎么理解:强缓存、协商缓存和 304
围绕HTTP 缓存章节,讲清浏览器为什么要缓存、强缓存和协商缓存如何判断、Cache-Control、ETag、Last-Modified 分别解决什么问题。
相关工具
缓存不是偷懒,是减少重复传输
HTTP 缓存解决的是一个很朴素的问题:同一个资源已经下载过了,下次还要不要重新从服务器拿一遍?文中对缓存的定义很清楚:客户端或代理服务器会把已经请求过的资源副本保存在本地,下次请求同一资源时,先检查缓存里有没有有效副本。如果有,就直接使用缓存,不必再通过网络获取服务器响应。
这件事对网页体验影响很大。图片、CSS、JavaScript、字体文件通常不会每秒都变化,如果每次打开页面都重新下载,既浪费带宽,也拖慢首屏,还会增加服务器压力。缓存命中后,浏览器可以少发请求,或者只发一个很轻的验证请求,页面自然更快。
不过缓存也容易带来误会。开发时你明明改了文件,浏览器却还展示旧内容;接口返回 304,响应体为空,有人以为服务端坏了;设置了 no-cache,又以为完全不缓存。要把这些问题说清楚,先分清两条路:强缓存和协商缓存。

浏览器先判断强缓存,强缓存不可用时,再带条件请求和服务器协商。
强缓存:没过期就不问服务器
强缓存的特点是“自己判断”。浏览器根据响应头里的缓存规则,判断目标资源是否还在有效期内。如果命中强缓存,就直接从内存缓存或磁盘缓存读取资源,不需要和服务器通信。文中也写到,命中强缓存时可以直接从内存中读取目标资源,无需与服务器做任何通信。
早期常用 `Expires` 控制强缓存。它给资源设置一个绝对过期时间,在这个时间范围内,浏览器可以直接使用缓存。但它有一个明显问题:判断依赖客户端本地时间。如果用户电脑时间不准,或者中间环境时间异常,缓存判断就会变得不可靠。所以 `Expires` 现在更多是兼容性角色。
HTTP/1.1 里更常用的是 `Cache-Control`。比如 `Cache-Control: max-age=3600` 表示资源可以缓存 3600 秒。它不是拿本地绝对时间和服务器给的过期时间硬比,而是按相对时长判断,更适合实际使用。
Cache-Control 里几个字段别混着背
`max-age` 决定客户端缓存多久。静态资源如果带 hash 文件名,比如 `app.8f3a.js`,通常可以给比较长的 max-age,因为文件内容一变,文件名也会变。用户拿旧文件名命中缓存并不危险,新页面会引用新文件名。
`s-maxage` 面向代理缓存,比如 CDN 或中间代理服务器。它和浏览器本地缓存不是一回事。`public` 表示资源既可以被浏览器缓存,也可以被代理服务器缓存;`private` 表示资源只能被浏览器缓存,默认更偏向 private。涉及用户个人信息的响应,不应该随便让共享缓存保存。
`no-cache` 很容易被误解。它不是完全不缓存,而是要求使用缓存前必须先和服务器协商确认。真正禁止任何缓存的是 `no-store`。登录态、支付结果、敏感个人信息这类响应,如果不希望落到任何缓存里,应该考虑 no-store,而不是只写 no-cache。

强缓存主要看 Expires 和 Cache-Control;协商缓存主要看 Last-Modified、ETag 及对应请求头。
协商缓存:先问一句资源变没变
当强缓存没命中,或者缓存规则要求必须验证时,浏览器就会走协商缓存。协商缓存不是直接重新下载资源,而是把上一次服务器给的资源标识带回去,让服务器判断资源有没有变化。
如果服务器判断资源没有变化,就返回 `304 Not Modified`。这个响应通常没有响应体,意思是:你本地那份还能用,别重新下载了。上一章状态码里提到过 304,它必须和缓存放在一起理解。看到 304,不要急着找后端为什么没返回 body,它本来就是为了省掉 body。
如果服务器判断资源已经变化,就返回 `200 OK` 和新的资源内容,同时更新相关缓存头。浏览器拿到新资源后,会把它保存下来,供下一次请求继续判断。
Last-Modified:按修改时间判断
第一种协商缓存方式,是 `Last-Modified` 配合 `If-Modified-Since`。服务器第一次返回资源时,在响应头里带上 `Last-Modified`,表示资源最后修改时间。浏览器下一次请求同一资源时,把这个时间放进请求头 `If-Modified-Since`。
服务器收到请求后,会拿请求里的时间和当前资源的最后修改时间比较。如果资源没有更新,就返回 304;如果资源更新了,就返回 200 和新内容。这个流程很好理解,也很容易排查:响应头给时间,请求头带时间,服务器比较时间。
它的问题也在 文中说得很具体:文件内容没变,但修改时间可能变了,比如文件名改来改去,缓存会被误判失效;反过来,如果文件在很短时间内完成多次修改,而修改时间记录精度不够,也可能内容已经变了,时间却没体现出来。
ETag:按资源指纹判断
`ETag` 解决的是时间不够精确的问题。服务器根据资源内容计算出一个标识,也可以理解成资源指纹,第一次响应时放在 `ETag` 头里。浏览器下次请求时,把上次的 ETag 放进 `If-None-Match`。
服务器再计算当前资源的 ETag,和请求里的 `If-None-Match` 比较。如果一致,说明资源没变,返回 304;如果不一致,说明资源变了,返回 200 和新资源。文中也提到,相比 Last-Modified,ETag 的优先级更高,因为它判断的是资源指纹,而不只是修改时间。
ETag 也不是没有成本。资源很大、数量很多、计算频繁时,生成指纹会增加服务器计算开销。还有强验证和弱验证的区别:强验证更精确,但更费计算;弱验证速度更好,但准确度会低一些。实际工程里,要在准确性和成本之间取舍。
缓存排查时看这几组头
排查缓存问题时,先看响应头有没有 `Cache-Control`。如果有 `max-age`,就判断资源是不是还在强缓存有效期内。如果写了 `no-cache`,不要以为它没缓存,而是说明下一次要走协商。如果写了 `no-store`,才表示不保存任何缓存内容。
再看协商缓存字段。响应头里如果有 `ETag`,下一次请求通常会带 `If-None-Match`;响应头里如果有 `Last-Modified`,下一次请求可能会带 `If-Modified-Since`。服务端返回 304,说明资源未变化;返回 200,说明资源重新下发,缓存也会被更新。
最后看资源类型和 URL。带 hash 的静态资源适合长缓存;HTML 入口文件通常不能缓存太久,否则用户可能拿不到最新资源引用;接口响应要更谨慎,尤其是带用户身份、权限、余额、订单状态的数据。缓存不是越久越好,关键是资源是否可复用、是否会变、变了以后能不能通过 URL 或校验机制识别。
一套更稳的理解方式
可以把 HTTP 缓存记成两句话:强缓存看“本地规则是否还有效”,有效就不请求服务器;协商缓存看“服务器确认资源有没有变化”,没变化就返回 304,有变化就返回 200 和新内容。
`Expires` 和 `Cache-Control` 主要负责强缓存,其中 `Cache-Control` 是现在更常用的方式。`Last-Modified / If-Modified-Since` 和 `ETag / If-None-Match` 主要负责协商缓存,其中 ETag 更精确,但可能带来计算成本。
真正做项目时,不要只问“要不要缓存”,而要问三个更具体的问题:这个资源能不能复用?它变化后 URL 会不会变?如果 URL 不变,服务器能不能准确告诉浏览器它有没有变?这三个问题想清楚,缓存策略基本就不会乱。
常见问题
304 是错误吗?
不是。304 表示资源没有修改,浏览器可以继续使用本地缓存,所以响应体通常为空。
no-cache 是不缓存吗?
不是。no-cache 表示可以存缓存,但使用前必须和服务器协商确认;no-store 才是不保存缓存。
ETag 一定比 Last-Modified 好吗?
ETag 判断更精确,优先级通常更高,但计算资源指纹会增加服务器开销,资源很大或数量很多时要权衡。