第一阶段/1.0 时代与 Expires 的致命缺陷
🕰️ 背景 (Context)
在早期的 Web 时代(1996 年左右,HTTP/1.0),网页多是纯静态文档,网速极慢。为了减少服务器压力和加快页面加载,W3C 和 IETF 的专家们决定在浏览器里加个本地缓存。
🛠️ 解决方案 (强缓存)
服务器在下发资源时,在 HTTP 响应头里加上一个绝对时间:
Expires: Wed, 21 Oct 2026 07:28:00 GMT
浏览器的脑回路很简单:“在这个绝对时间点到来之前,我再要这个文件,就不找服务器了,直接从本地硬盘读。”
💥 核心痛点 (为什么必须演进?) 依赖的是客户端本地的系统时间!
如果用户的电脑主板电池没电了,或者用户自己把电脑时间调到了 2027 年,那么这个缓存机制就瞬间全部失效,或者永远都不去请求新资源。
依赖客户端时间来做服务端缓存控制,在工程上是一个极其脆弱的设计。
第二阶段/1.1 时代与 Cache-Control 的救场
🕰️ 背景 (Context)
1999 年,HTTP/1.1 规范发布。互联网变得更加动态,专家们痛定思痛,决定彻底解决客户端时间不准导致的缓存车祸。
🛠️ 解决方案 (强缓存 2.0)
不再使用绝对时间,而是引入了**相对时间(滑动窗口)**的概念:
Cache-Control: max-age=3600
浏览器的脑回路变成了:“从我接收到这个文件的这一秒开始算,接下来的 3600 秒(1 小时)内,我直接用本地缓存。”
💡 上下文的精妙之处 (面试核弹)
- 完美避坑:
max-age彻底摆脱了对客户端系统时钟的依赖,依靠相对倒计时,完美解决了Expires的缺陷。 - 向下兼容与优先级 HTTP/1.0 浏览器,现代服务器通常会同时下发
Cache-Control和Expires。但在 HTTP/1.1 规范中被硬性规定:只要Cache-Control存在,Expires就会被无视。这就是为什么面试题里会有”优先级谁高”的由来——不是凭空定规矩,而是新旧时代的权力交接。
第三阶段? —— 协商缓存的演进
强缓存(max-age)虽然爽,但倒计时总有结束的时候。
倒计时结束后,浏览器必须去问服务器:“老哥,这个文件更新过吗?”
这就引出了协商缓存,而它同样经历了一次从 HTTP/1.0 到 1.1 的填坑演进。
填坑前/1.0 的 Last-Modified (基于修改时间)
做法
服务器首次返回文件时,带上 Last-Modified: [文件最后修改的绝对时间]。缓存过期后,浏览器拿着这个时间去问服务器(通过 If-Modified-Since 请求头):“从这个时间之后,文件改过吗?”
如果没改,服务器返回 304 Not Modified,告诉浏览器继续用旧文件。
痛点 (为什么又不行了?)
- 精度太粗。如果一个文件在 1 秒内被修改了 5 次,
Last-Modified根本察觉不到,会导致用户拿到旧文件。 - 内容没变,但时间变了,哪怕文件内容连一个标点符号都没变,修改时间也会变,导致浏览器傻傻地重新下载。
填坑后/1.1 的 ETag (基于内容指纹)
为了解决时间的精度和误判问题,HTTP/1.1 再次引入了终极杀器。
做法
服务器直接根据文件的内容,计算出一个哈希值(指纹),比如:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
优势
缓存过期后,浏览器拿着这个指纹去问服务器(If-None-Match)。服务器只要重新算一下当前文件的指纹对比一下就行。
- 只要内容改了哪怕一个字节,指纹绝对不同
- 内容没变,哪怕修改时间变了一万次,指纹也一样
优先级重演
正如 Cache-Control 压制 Expires 一样,由于指纹比时间更精准,ETag 的优先级也硬性高于 Last-Modified。
总结”上下文”征服面试官的话术
“浏览器缓存可以从 HTTP 协议演进的上下文来看。它主要分为强缓存和协商缓存。
在 HTTP/1.0 时代,强缓存靠
,后者精度只能到秒且无法识别内容本质的改变。Expires的绝对时间,协商缓存靠Last-Modified的修改时间。但这俩都有致命的工程缺陷所以 HTTP/1.1 针对这两个痛点进行了重构
Cache-Control: max-age这个相对时间来替代Expires,同时引入了基于内容哈希的ETag来弥补Last-Modified的缺陷。为了保证平滑过渡和向下兼容,协议规范硬性指定了新机制的优先级高于老机制。最终在浏览器里的运作流程就是
Cache-Control决定要不要发请求;如果要发,就带上ETag去服务端进行指纹协商,根据状态码是 200 还是 304 来决定是重新下载还是复用本地资源。“
缓存工作流程图
用户请求资源
↓
检查本地缓存是否存在
↓
存在 → 检查 Cache-Control (max-age)
↓
未过期 → 直接使用本地缓存 (强缓存命中)
↓
已过期 → 发起协商缓存请求
↓
携带 If-None-Match (ETag) 或 If-Modified-Since
↓
服务器对比
↓
├─ 304 Not Modified → 继续使用本地缓存
└─ 200 OK → 下载新资源并更新缓存