浏览器本地就有一层缓存。
它分两条路:强缓存(自己判断还能用,一个字节都不发)和协商缓存(发个条件请求,
服务端回 304 省下正文)。
上一节说的是「缓存这个角色怎么判断有效期」。这一节把缓存搬到最近的地方——用户自己的机器上。
一、客户端的缓存在哪
书里管它叫「临时网络文件」。 今天的浏览器里它分散在好几处:内存缓存(关标签页就没了)、磁盘缓存(几个 GB,按域分目录)、 还有专门为「后退」准备的往返缓存(给你瞬间回到上一页的错觉)。 位置不重要,重要的是它替你省掉了整个请求。这一层命中时,服务器日志里连一条记录都不会出现。 所以「我明明改了文件,怎么用户还是旧的」这类问题,十次有八次出在这一层。
二、两条路:强缓存与协商缓存
强缓存:本地那份还在有效期内,浏览器直接拿来渲染,不发任何网络请求。
用户看到的可能就是几天前的内容。
协商缓存:有效期过了,但内容可能没变。
浏览器就发一个条件请求,把上次拿到的凭证带上去问一句「还是这个吗」。
服务端说没变(304),浏览器就用本地那份;说变了(200),就传新的。
这两条路的成本差得很远。Demo 里三次访问同一个 754 字节的页面,没有缓存时来回 2679 字节;
走强缓存只有 923(等于只访问了一次);走协商缓存 1336 字节——请求照发,省下来的只是正文。
三、两种凭证
| 凭证 | 响应里给 | 下次请求带 | 精度 |
|---|---|---|---|
| 修改时间 | Last-Modified |
If-Modified-Since |
秒 |
| 实体标签 | ETag |
If-None-Match |
任意 |
Last-Modified 只精确到秒,这带来一个具体的坑: |
|||
文件在同一秒内改了两次,第二次改不出来——时间戳没变,服务端会认为没改,回 304, |
|||
| 客户端一直拿旧文件。这是很多构建工具和 CDN 的真实故障原因。 | |||
ETag 没有这个问题,它可以是任何字符串,比如内容哈希。代价是服务端要为每个资源算一个值。 |
|||
ETag 还分强弱:ETag: "v7" 是强标签,字节级完全相同; |
|||
ETag: W/"v7" 是弱标签,表示语义等价但字节可能有细微差别(比如压缩过)。 |
|||
带 W/ 前缀的不能用于断点续传那种要求字节一致的场景。 |
四、动手 Demo:四次访问、四条路的字节对比
同一个 754 字节的页面,用四种方式各访问三次:完全没有缓存指令、带 max-age=3600、
带 Last-Modified 加条件请求、带 ETag 加条件请求。统计请求次数和总字节数。
# 服务端:认 If-None-Match 就回 304
if path == u'/etag':
inm = hdrs.get(u'if-none-match', u'')
if inm == u'"v7"':
return reply(u'HTTP/1.1 304 Not Modified',
[(u'ETag', u'"v7"')], b'')
return reply(u'HTTP/1.1 200 OK',
[(u'ETag', u'"v7"')], DOC)
# 客户端:第一次裸请求,之后带上凭证
for i in range(3):
extra = [(u'If-None-Match', u'"v7"')] if i else []
r = req(u'GET', u'/etag', u'c.example', extra)
out = ask(port, r)
实跑输出:
--------------------------------------------------------------------
客户端缓存的两条路:不发请求,或者只回一个 304
--------------------------------------------------------------------
资源正文长度 754 字节
无缓存指令 三次 请求 3 次 | 字节 2679
强缓存三次 请求 1 次 | 字节 923
第二次起带的条件头 If-Modified-Since: Sat, 25 Oct 2026 06:00:00 GMT
协商缓存三次 请求 3 次 | 字节 1336
ETag 三次 请求 3 次 | 字节 1182
同样三次的条件请求 ETag 比 Last-Modified 少 154 字节
一个 304 响应 79 字节
文本 HTTP/1.1 304 Not Modified\r\nETag: "v7"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n
十六 48 54 54 50 2f 31 2e 31 20 33 30 34 20 4e 6f 74 20 4d 6f 64 69 66 69 65 64 0d 0a 45 54 61 67 3a 20 22 76 37 22 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 63 6c 6f 73 65 0d 0a 0d 0a
服务端总请求数 11 次
一次全量来回 893 字节(请求 40 + 响应 853)
一次条件来回 139 字节(请求 60 + 响应 79)
每次省下 84.4%
强缓存:客户端自己判断还能用,一个字节都不发,最省但也最容易看到旧内容。
协商缓存:一定要问一句,服务端回 304 说明没变,省下的是正文而不是往返。
所以真正省时间的是强缓存;协商缓存省的是流量,往返照样花。
四次的总字节:无缓存 2679、强缓存 923、Last-Modified 协商 1336、
ETag 协商 1182。强缓存第三次访问是 0 字节,因为它压根没发请求。
强缓存省的是往返,协商缓存省的是流量。协商缓存三次请求全都发出去了,
服务端也全都处理了——时间一点没省(延迟反而因为这层逻辑稍微多一点),只是正文没传。
这个区别决定了你该选哪条路。
ETag 三次比 Last-Modified 三次少 154 字节。
原因是两边的字符串都更短:
If-None-Match: "v7" 比 If-Modified-Since: Sat, 25 Oct 2026 06:00:00 GMT 短得多,
响应里的 ETag: "v7" 也比 Last-Modified: ... 短。请求要来回很多次的东西,这个差额会积起来。
一个 304 响应是 79 字节,全量来回是 893 字节(请求 40 加响应 853)。每次省 84.4%。
30 秒实验
- 把
Last-Modified换成同一秒内的两个不同内容,看服务端会不会误判没改。 - 把
ETag换成W/"v7",看客户端能不能正确匹配(本 Demo 是字符串比较,能)。 - 想一想:
index.html配max-age=3600会有什么后果?发版之后用户多久能看到新版本?
常见误区
- 「协商缓存也能省时间」——往返照样有,省的是流量。
- 「
304是错误码」——它是成功的一种,只是「没有正文」。 - 「按 F5 就等于强制刷新」——F5 会带条件请求重新验证,Ctrl+F5 才是不走缓存。
自测三问(点开对答案)
1. 强缓存和协商缓存,哪个能省掉网络往返? —— 强缓存。协商缓存仍然要把条件请求发出去,只是响应里没有正文。 2. `Last-Modified` 的精度问题会带来什么故障? —— 同一秒内改两次,时间戳没变,服务端会回 304,客户端一直拿旧文件。 3. `ETag` 前面的 `W/` 是什么意思? —— 弱标签,表示语义等价但字节可能不完全相同,不能用于要求字节一致的场景。第五章到此结束。下一章是本书最厚的一章:把 HTTP 首部字段一个一个过一遍。
- ☐ (本章完,下一节 6.1 HTTP报文首部)