怎么让服务器回 304 not modified:一次重访只花一次响应头
304 not modified 是服务器回一句「什么都没变」,而且没有正文。Google 支持两对校验头、明确偏好 ETag,并公布它的抓取里只有 0.017% 可缓存。四步 SOP、一段两次请求的自检、一张五行判读表。

304 not modified 是服务器告诉爬虫「什么都没变,你手上那份还能用」,而且这条响应没有正文。发出它,一次重复抓取的成本就从「渲染整个页面」降到「交换一次响应头」。Google 通过两对头支持这件事,明确偏好其中一对,并且公布了一个说明几乎没人在用它的数字:它的抓取里只有 0.017% 是可缓存的。
读这篇之前
这一篇是抓取量那一串的收尾。前面几篇管的都是「地址是从哪来的」—— 筛选组合是一台造网址的机器,翻页序列是另一台,写在翻页怎么做。这一篇不动地址数量,处理的是另一半:已经存在的那些地址,每被重访一次要花你多少。如果你还没判断过抓取量到底是不是你的问题,前置的那一问写在抓取预算是给谁看的,对很多站来说答案是「不是」。
一条 304 not modified 到底省下什么
两笔成本,Google 在同一句话里把两笔都点了名。爬虫手上握着上次访问留下的校验值,下次请求会把它带回来;如果这个值还对得上,你的服务器只回一个状态行加若干响应头就停下 —— 没有正文,没有渲染,背后也没有那次数据库读取。
官方把收益直接写成了钱:「your server doesn't have to spend compute resources on actually generating content; that is, you save money」以及「your server doesn't have to transfer the HTTP body; that is, you save money」(Crawling December: HTTP caching,发布于 2024-12-09,2026-09-08 访问)。
被支持的机制很窄,值得原样写清楚。Google 的抓取基础设施支持启发式 HTTP 缓存,「specifically through the ETag response- and If-None-Match request header, and the Last-Modified response- and If-Modified-Since request header」。就这两对。缓存家族里其他的头都替代不了它们。
304 是唯一一种「少干活才是正确答案」的响应。
这件事被浪费掉的规模也在同一篇里,而且这是最值得随身带的一个数:「10 years ago about 0.026% of the total fetches were cacheable, which is already not that impressive; today that number is 0.017%.」这是 Google 自己在全网口径下的比例,不是对你站点的测量。而且它是降下来的。
顺带说一句它为什么容易被忽略:做 Web 的人天天在浏览器开发者工具里看见 304,那是浏览器缓存的语境。同一个机制换成爬虫来问,性质就从「页面加载快一点」变成了「你的服务器少被重复榨一次」。
27 个大站首页实际发了什么
2026-09-08 我们读了 27 个大站首页的响应头,每个站一次请求,桌面 Chrome UA。两个校验值都发的,只有三个 —— 而那正是官方文档要的配置。
| 首页发了什么 | 站数 | 答得了 304 吗 |
|---|---|---|
| 只有 ETag | 10 | 能,靠 If-None-Match |
| 只有 Last-Modified | 3 | 能,靠 If-Modified-Since |
| 两个都有 | 3 | 能,两条路都行 |
| 两个都没有 | 11 | 没有可带回的值 |
27 个里有 11 个不发任何能被带回来的东西。这些不是配坏了的站 —— 一个每小时变好几次的首页,完全有正当理由不发校验值。真正的问题是:这个决定通常根本没人做过,响应头是框架发什么就是什么。
还有一个数,照报但不下判断。面板上 13 个 ETag 里有 8 个是 W/ 开头的弱校验值。Google 文档只说 ETag 可以设成「any arbitrary ASCII string」,完全没有讨论弱形式,所以我们不会告诉你它有影响。它只是你查自己响应头时会看到、然后开始纳闷的一个东西。那一批面板更完整的响应头画面写在27 个首页的 HTTP 缓存头里。
动手做:四步
第一步是测量而不是改动,而且它决定后面还需不需要做。每一步都有你自己能查的信号。
- 先搞清楚你的服务器现在答不答 304。取一个页面,留下校验值,带着它再问一次。完成标志:你已经亲眼看到状态行是
304,或者是一个完整的200—— 两个都是真答案,后者说明有活要干。 - 没有校验值就加一个 ETag。Google 的偏好写得很直接:「We strongly recommend using ETag because it's less prone to errors and mistakes (the value is not structured unlike the Last-Modified value).」内容哈希或构建版本号都算合格取值。完成标志:新取一次返回了 ETag,且同一个页面连续两次返回的值相同。
- 定义什么才算「变了」。这是 Google 明确交还给你的判断,还附了一个例子:「Our recommendation is that you require a cache refresh on significant changes to your content; if you only updated the copyright date at the bottom of your page, that's probably not significant.」拿整页渲染结果算出来的 ETag,会在页脚年份变化时跟着变,这是最常见的翻车点。完成标志:你说得出哈希里到底放了哪些东西。
- 如果同时发 Last-Modified,日期格式要一字不差。官方既给了规则也给了形状:日期「must be formatted according to the HTTP standard」,为避免解析问题推荐写成「Weekday, DD Mon YYYY HH:MM:SS Timezone」,示例是
Fri, 4 Sep 1998 19:15:56 GMT。完成标志:你的响应头逐字符对得上这个形状。
交付物:一段自检和一张判读表
整个验证就是两次请求。挑一个不常变的页面来跑,因为一个每分钟都在变的页面本来就该回 200。
# 1. 取校验值
curl -sI https://example.com/some-stable-page \
| grep -iE '^(etag|last-modified|cache-control):'
# 2. 把它带回去,读状态行
curl -sI https://example.com/some-stable-page \
-H 'If-None-Match: "PASTE-THE-ETAG-HERE"' | head -1
# 期望:HTTP/2 304
# 如果拿到带完整正文的 200,说明条件请求根本没被处理
| 现在发什么 | 该做什么 | 为什么 |
|---|---|---|
| 两个都没有 | 加一个 ETag | 今天没有可校验的东西 |
| 只有 Last-Modified | 加 ETag,两个都留 | 官方偏好 ETag 且建议都设 |
| 只有 ETag | 保持,或补 Last-Modified | 已经能应答 If-None-Match |
| 两个都有但每次回 200 | 改服务器,不是改头 | 发校验值不等于处理校验值 |
| 页面每小时都在变 | 不加校验值 | 304 答对的次数会少于答错 |
还有一个可选项,官方特意标了它是可选的:「While not required, consider also setting the max-age field of the Cache-Control header to help crawlers determine when to recrawl the specific URL.」这个值的含义是你预计内容会保持不变多久。
做错的三种方式
这三种从外面看响应头都是对的,所以那条两次请求的自检比翻配置文件值钱。前两种是白费力气。第三种会悄悄让你的更新推不出去 —— 如果你改过的页面在搜索结果里还是旧的,先查它。
- 发了校验值却不处理它。最常见的一种,也是第一步要做成测量的原因。框架吐出一个 ETag,条件请求来了,应用照样重建页面回 200。从外面看响应头没毛病,实际什么也没省下。
- 校验值每次请求都变。如果 ETag 是从任何会变的东西算出来的 —— 时间戳、一次性随机值、带随机名的静态资源 —— 它就永远对不上,每次条件请求都变成一次完整响应外加一趟白跑的往返。抓它的办法是对同一个没改过的页面取两次,比对两个值。
- 校验值永远不变。相反方向的失败,而且更严重。官方文档写明 304「will happen to every subsequent request until the preconditions fail to validate」,所以一个冻住的 ETag 意味着页面明明更新了,服务器还在说「什么都没变」。这次丢的不是带宽,是更新。
这一篇到哪里为止
官方给了机制,外加一个全网统计。它没有公布这件事在某个具体站上值多少,我们也给不出来。
- 这能换来多少抓取。没有数字,也没有办法从站外推出你这个站的数字。官方自己的措辞是条件式的:缓存「may help your site be crawled more efficiently」,而且限定在内容很少变化的大站上。那不是承诺,我们也不打算把它升级成承诺。
- 会不会影响排名。官方文档没有把条件响应和排名连起来,我们没做过实验,在线上站也隔离不出这个变量。省下来的是你的服务器账单和抓取负载。
- 你的 CDN 在这中间干了什么。挡在源站前面的缓存可能自己生成校验值、剥掉你的,或者干脆自己应答条件请求。我们的测量读的是到达客户端的东西,分不出是哪一层产生的。如果绕过边缘节点之后响应头变了,那一层就是该去读的地方。
- 其他爬虫是不是一样的行为。上面记载的是 Google 的。AI 爬虫拿到条件请求会怎么做,我们没测过,它们也没有同等详细的公开说明。
常见问题
304 not modified 是什么意思?
它是一条「你手上那份还是最新的」的响应,只有响应头、没有正文。拿到它的爬虫会继续用自己存的那份,你的服务器则跳过生成页面这一步。
该用 ETag 还是 Last-Modified?
按 Google 表态用 ETag,因为它的值没有结构,比一个格式化日期更难写错。同一篇还补了一句:条件允许就两个都设。我们这次的面板上,27 个首页里只有 3 个这么做。
回 304 会让 Google 更频繁地抓我的站吗?
没有任何公开说明支持这个说法。写明的收益在你这一侧 —— 不用生成页面、不用传正文。任何「能提高抓取频率」的承诺都当作没有依据。
我的站发了 ETag 但从来不回 304,问题在哪?
几乎总是在应用层,不在响应头。吐出一个 ETag 和比对进来的 If-None-Match 是两件独立的工作,很多技术栈默认做了第一件、第二件从来没做。上面那条两次请求的自检,十秒钟就能把两者分开。
如果爬虫根本够不到这个页面,这些还有用吗?
没用,而且那一层才该先查。条件请求是叠在可达性上面的优化,爬虫如果在看到响应头之前就被拒了,上面这些都无从谈起。这一半可以用 AI 爬虫可达性检查先回答掉。
本文属于 QueryWin 实操手册 · 第 3 阶


