# HTTP的缓存控制
30 秒速记
- 缓存两条路:还新鲜就直接用(强缓存),过期了带着版本号去问服务器(协商缓存),没变就回
304 - 服务器用
Cache-Control定规矩:max-age定保鲜期,起点是响应生成时刻,路上耽误的时间也算 no-store不许存;no-cache能存但每次用之前都要问;must-revalidate过期了才必须问- 协商靠验证器:
Last-Modified配If-Modified-Since,ETag配If-None-Match,ETag更精确 - 刷新会改请求:普通刷新主文档带
max-age=0去验证,强制刷新带no-cache且不带验证头,直接拿200
HTTP 缓存就两步:东西还新鲜就直接用,过期了就带着版本号去问服务器「我这份还能用吗」。 新鲜度由服务器在 Cache-Control 里定,max-age=3600 就是一小时内不用问;过期后浏览器带上 If-None-Match 或 If-Modified-Since 去验证,没变服务器回个 304,不传正文。最容易搞混的是 no-cache,它不是不缓存,是每次用之前都要先问一下,真不让存的是 no-store。项目里最常见的配法是:带 hash 的静态资源 max-age=31536000, immutable,HTML 用 no-cache。
面试官追问
追问 1静态资源到底要不要用 ETag?
看资源怎么命名。文件名带内容 hash 的,配一年 max-age 加 immutable,根本不会走到协商,ETag 有没有无所谓;HTML 这种地址不变、内容会变的,就要靠 ETag 或 Last-Modified 做协商缓存。
追问 2发版后有用户一直看到旧页面,最可能是哪里配错了?
先看 HTML 的响应头。HTML 被设了长 max-age,浏览器就一直用旧 HTML,旧 HTML 里引的也是旧 JS。HTML 改成 no-cache,静态资源靠文件名 hash 更新。
追问 3浏览器、代理、CDN 的缓存行为都一样吗?
不一样。private 只允许浏览器存,CDN 不能存;s-maxage 只给共享缓存用,会覆盖 max-age;CDN 自己还有控制台里的缓存规则,可能直接无视源站的头。排查时三层要分开看。
追问 4服务器什么缓存头都不给,浏览器就不缓存了吗?
会缓存。有 Last-Modified 时浏览器走启发式缓存,一般按 (Date - Last-Modified) × 10% 算保鲜期,一个三个月没改的文件可能被缓存好几天。所以别不写,要明确写出来。
# 服务器的缓存控制
30 秒速记
- 流程:浏览器没缓存 → 请求 → 服务器响应并用
Cache-Control标保鲜期 → 浏览器存起来下次复用 max-age是存活时间,从响应生成时刻(Date)算起,链路和中间缓存里待的时间也扣no-store= 西瓜不许进冰箱;no-cache= 能进冰箱,吃前先问超市;must-revalidate= 保鲜期内随便吃,过期了必须问- 其他常用:
private只许浏览器存,public共享缓存也能存,s-maxage专给CDN,immutable表示有效期内连刷新都不用验证 - 实战配法:带
hash的资源max-age=31536000, immutable,HTML用no-cache,含个人数据的接口private或no-store
服务器通过 Cache-Control 告诉浏览器「这份东西能存多久、存了之后怎么用」。 最常用的是 max-age,单位是秒,注意它从响应在服务器生成的那一刻开始算,路上走了 4 秒,max-age=5 到浏览器手里就只剩 1 秒。no-store、no-cache、must-revalidate 名字像,意思差很远:no-store 是压根不存;no-cache 是存着但每次用都要先找服务器确认;must-revalidate 是保鲜期内直接用,过期后必须确认,不许拿过期的凑合。实际配置我按资源类型分:带 hash 的长缓存,HTML 走 no-cache,用户私人数据 private。
为了更好地说明缓存的运行机制,下面我用“生鲜速递”作为比喻,看看缓存是如何工作的。
夏天到了,天气很热。你想吃西瓜消暑,于是打开冰箱,但很不巧,冰箱是空的。不过没事,现在物流很发达,给生鲜超市打个电话,不一会儿,就给你送来一个 8 斤的沙瓤大西瓜,上面还贴着标签:“保鲜期 5 天”。好了,你把它放进冰箱,想吃的时候随时拿出来。
在这个场景里,“生鲜超市”就是 Web 服务器,“你”就是浏览器,“冰箱”就是浏览器内部的缓存。整个流程翻译成 HTTP 就是
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。

服务器标记资源有效期使用的头字段是“Cache-Control”,里面的值“max-age=30”就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用。”
你可能要问了,让浏览器直接缓存数据就好了,为什么要加个有效期呢?
这是因为网络上的数据随时都在变化,不能保证它稍后的一段时间还是原来的样子。就像生鲜超市给你快递的西瓜,只有 5 天的保鲜期,过了这个期限最好还是别吃,不然可能会闹肚子。
“Cache-Control”字段里的“max-age”和上一讲里 Cookie 有点像,都是标记资源的有效期。
但我必须提醒你注意,这里的 max-age 是“生存时间”(又叫“新鲜度”“缓存寿命”,类似 TTL,Time-To-Live),时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。
比如,服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。
“max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存
no_store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;must-revalidate:又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下:
no_store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;no_cache:可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;must-revalidate:可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃
把服务器的缓存控制策略画了一个流程图,对照着它你就可以在今后的后台开发里明确“Cache-Control”的用法了

原理拆解: 响应进入浏览器缓存或共享缓存后,缓存会根据 Date、已驻留时间和 max-age 计算当前年龄。年龄小于新鲜度寿命时可直接复用;超过寿命只代表响应已经陈旧,并不等于缓存条目被删除。陈旧响应若带有 ETag 或 Last-Modified,通常可以发起条件请求:资源未变化时服务器返回 304 Not Modified,缓存继续使用原响应体;发生变化时返回新的 200 响应和实体。
最小验证: 输入为同一路径的两次访问,第一次响应允许缓存 30 秒并提供验证器;要验证的结论是新鲜期内可复用,过期后必须验证,而不是无条件重新下载正文。
GET /app.js HTTP/1.1
Host: example.test
HTTP/1.1 200 OK
Date: Mon, 17 Aug 2026 08:00:00 GMT
Cache-Control: max-age=30, must-revalidate
ETag: "app-v7"
Content-Type: application/javascript
Content-Length: 18
console.log("v7");
GET /app.js HTTP/1.1
Host: example.test
If-None-Match: "app-v7"
HTTP/1.1 304 Not Modified
Date: Mon, 17 Aug 2026 08:00:31 GMT
ETag: "app-v7"
Cache-Control: max-age=30, must-revalidate
关键行是 ETag 与 If-None-Match:预期第二次交换没有响应体,说明验证成功后复用了已存实体。如果资源已更新,预期应为 200 OK 和新正文;若没有验证器,过期后通常只能重新获取完整响应。
边界与排查: no-store 才是禁止存储;no-cache 允许存储,但正常使用前必须验证。must-revalidate 只约束陈旧响应,新鲜期内仍可直接复用。响应还可能经过代理或 CDN,因此排查时应同时查看 Age、Date、Cache-Control
