用浏览器开发者工具或抓包工具看网站请求,会发现图片、CSS、JS 这类静态资源经常返回 304 Not Modified。不少新手看到 304 会紧张,以为是不是出错了,或者跟 404 一样是异常。其实恰恰相反:304 是 HTTP 协议里"我很省"的体现——服务器告诉浏览器"你本地缓存的文件还是最新的,直接用,不用重新下载",是协商缓存命中时返回的状态码,对性能只有好处。
这篇文章把 304 讲透:先解释 304 到底是什么、为什么能省流量,再梳理浏览器缓存的完整流程(强缓存与协商缓存的区别),然后分别讲清 Last-Modified/If-Modified-Since 和 ETag/If-None-Match 两套协商机制的原理与对比,接着给出 Nginx 里 etag、expires、Cache-Control 的真实配置方法,最后落到实战:什么情况 304 是好事、什么情况它反而会坑你,以及"改了文件浏览器不更新"这类高频问题怎么排查。
核心结论先给出:304 不是错误,而是协商缓存命中;它省的是传输流量,但每次仍要发一次请求去问服务器。真正极致省流量的是强缓存(根本不发请求)。理解这两者的区别,才能把缓存策略配对。
一、304 到底是什么
HTTP 状态码 304(Not Modified)属于 3xx 重定向类,但它不跳转地址,而是表达一个语义:客户端发来的条件请求表明,本地缓存版本仍未过期,服务器无需返回内容。
关键特征:
- 不是错误:不像 404(找不到)、403(无权限)、500(服务器错误),304 是正常且期望发生的结果。
- 响应没有 body:304 响应体为空,只带少量头部,所以极省带宽。
- 需要客户端主动发起:客户端要先带上"条件"(上次拿到的 ETag 或 Last-Modified),服务器才可能回 304。
- 仍是一次网络请求:这点常被忽略——304 省的是 body,请求本身(DNS、TCP、可能的 TLS、协商)还是要走的。
所以 304 与"完全不请求"完全是两码事。
二、浏览器缓存的完整流程
要理解 304,必须先理解浏览器的两级缓存:强缓存和协商缓存。
浏览器请求资源
│
├─ 有强缓存且未过期? ── 是 ──> 直接用本地缓存(200 from cache),不发请求
│
└─ 否 ──> 发起请求,带上 ETag / Last-Modified
│
├─ 服务器判断未变 ──> 返回 304,浏览器用本地缓存
│
└─ 服务器判断已变 ──> 返回 200 + 新内容
两种缓存的对比:
| 类型 | 相关响应头 | 是否发请求 | 生效条件 |
|---|---|---|---|
| 强缓存 | Cache-Control: max-age、Expires |
不发 | 本地缓存未过期 |
| 协商缓存 | ETag/If-None-Match、Last-Modified/If-Modified-Since |
要发 | 强缓存失效后向服务器确认 |
流程是:先看强缓存,强缓存过期(或没有)才走协商缓存。协商缓存命中就返回 304,未命中返回 200 带新内容。理解这个层级,就能明白 304 发生在"强缓存失效之后"。
三、Last-Modified / If-Modified-Since 机制
这是最早、也最简单的协商缓存方案,基于文件的最后修改时间。
- 服务器首次响应资源时,带上
Last-Modified: Wed, 01 Oct 2026 10:00:00 GMT。 - 浏览器下次请求同一资源时,在请求头带上
If-Modified-Since: Wed, 01 Oct 2026 10:00:00 GMT(就是上次那个时间)。 - 服务器比较:如果文件在这个时间之后没被修改过,返回 304;否则返回 200 和新内容,并更新
Last-Modified。
首次:
响应头 Last-Modified: Wed, 01 Oct 2026 10:00:00 GMT
再次:
请求头 If-Modified-Since: Wed, 01 Oct 2026 10:00:00 GMT
响应 304 Not Modified
局限:
- 精度只到秒:同一秒内改了两次,第二次改动感知不到。
- 时间可能不可靠:文件被重新部署、从版本库检出,
mtime变化但内容没变,会误判为"已修改",导致该 304 却 200。 - 只能表示时间先后,无法表达内容是否真的相同。
四、ETag / If-None-Match 机制
ETag(Entity Tag)是对文件内容的摘要标识,通常是内容哈希(Nginx 默认用 Last-Modified 时间 + 文件大小组合生成一个强 ETag)。
- 服务器首次响应带
ETag: "5f8a1b-1a2b-2c"。 - 浏览器下次请求带
If-None-Match: "5f8a1b-1a2b-2c"。 - 服务器比较 ETag,相同则 304,不同则 200 带新内容和新的 ETag。
首次:
响应头 ETag: "5f8a1b-1a2b-2c"
再次:
请求头 If-None-Match: "5f8a1b-1a2b-2c"
响应 304 Not Modified
优势:基于内容而非时间,文件内容没变即使 mtime 变了也能正确命中 304。ETag 分两类:
- 强 ETag:
ETag: "abc",要求内容逐字节一致。 - 弱 ETag:
ETag: W/"abc",允许语义等价(差别不显著)。
注意多台服务器的坑:如果静态资源由多台后端/Nginx 提供服务,且 ETag 生成算法里包含 Last-Modified 或 inode 等每台机器不同的信息,同一文件在不同机器上 ETag 会不一致,用户在机器间切换就会反复 200。解决方式是统一 ETag 生成规则,或用内容哈希。
五、两套机制对比与取舍
| 维度 | Last-Modified | ETag |
|---|---|---|
| 依据 | 文件修改时间 | 内容标识(哈希/时间+大小) |
| 请求头 | If-Modified-Since |
If-None-Match |
| 响应头 | Last-Modified |
ETag |
| 精度 | 秒级 | 内容级(更精确) |
| 误判风险 | mtime 变化但内容没变会误判 | 更准,但多机不一致会误判 |
| 服务器开销 | 小 | 需计算标识,略大 |
| 优先级 | 同时存在时 ETag 优先 | 优先 |
两者可以同时存在,此时浏览器会同时发送 If-None-Match 和 If-Modified-Since,服务器以 ETag 结果为准。多数场景保留两者即可,Nginx 默认都开。若遇到多机 ETag 不一致的顽固问题,可以考虑关闭 etag 只用 Last-Modified,或改用内容哈希方案。
六、Nginx 里的静态资源缓存配置
Nginx 有几个和缓存直接相关的指令:
server {
listen 80;
server_name static.example.com;
location ~* \.(jpg|jpeg|png|gif|css|js|woff2?)$ {
root /var/www/static;
# 协商缓存:ETag 默认开启(on)
etag on;
# 强缓存:告诉浏览器在本地直接用,不发请求
# 下面两行等价性:expires 会自动生成 Cache-Control max-age
expires 30d;
# 更精细地自定义 Cache-Control(会覆盖 expires 生成的)
# add_header Cache-Control "public, max-age=2592000, immutable";
# 关掉强缓存,强制每次走协商缓存(调试用)
# add_header Cache-Control "no-cache";
}
}
要点:
etag on:Nginx 默认开启 ETag,一般不用改。设为off可关闭。expires 30d:设置强缓存 30 天,同时生成Cache-Control: max-age=2592000和Expires头。30d是常见经验值;带内容哈希(文件名含指纹)的资源可以设很久(如 1 年),不带指纹的 HTML 建议设短或不缓存。add_header Cache-Control "no-cache":注意no-cache不是"不缓存",而是"可以缓存但每次必须向服务器确认",效果就是每次走协商缓存、可能返回 304。真正不缓存是no-store。- 改完校验重载:
nginx -t && nginx -s reload。
常见缓存头语义对照:
| 响应头 | 含义 |
|---|---|
Cache-Control: max-age=2592000 |
强缓存 30 天,期间不发请求 |
Cache-Control: no-cache |
可缓存,但每次必须协商(走 304) |
Cache-Control: no-store |
完全不缓存 |
Cache-Control: immutable |
声明内容永不变化,配合长 max-age 减少刷新请求 |
Expires: <日期> |
老式强缓存头,被 Cache-Control 优先覆盖 |
七、304 是好事还是"坑"?
多数情况下 304 是好事:省流量、省后端计算、加快二次访问。但它也有需要警惕的场景:
- 强缓存没设好,导致每次请求都走 304:虽然省了 body,但每次都要发请求+往返,比"根本不发请求"慢得多。理想做法是给带指纹的静态资源设长
max-age,让它们根本不发请求;只有 HTML 等易变资源才走 304。 - HTML 页面返回 304 导致内容不更新:如果 HTML 被缓存,用户改了内容后仍看到旧页面。HTML 通常应设
no-cache或很短的 max-age。 - 把 304 当成"没请求成功":开发时看到 304 就以为接口没返回,其实是缓存命中。调试时可在 DevTools 勾选 "Disable cache" 或强制刷新,看到 200。
八、排障:该 304 却 200、该更新却不更新
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 每次刷新都 200,不走 304 | ETag/Last-Modified 缺失或多机不一致 | 检查响应头;统一 ETag 规则;etag on |
| 文件改了浏览器还显示旧的 | 强缓存 max-age 太长 | 缩短 max-age,或给文件名加内容指纹 |
| 该 304 却总是 200 且反复下载 | ETag 随时间/机器变化 | 关闭 etag 用 Last-Modified,或统一算法 |
| 强制刷新(Ctrl+F5)看不到 304 | 强刷会跳过缓存 | 正常现象,用普通刷新或 DevTools 观察 |
| 部分用户正常部分不更新 | 多台服务器 ETag 不一致 | 统一多机的 ETag 生成,或用哈希文件名 |
用 curl 验证缓存头是否如预期:
# 看响应头里的 ETag / Last-Modified / Cache-Control
curl -I https://static.example.com/app.js
# 模拟带条件请求,验证是否返回 304
ETAG=$(curl -sI https://static.example.com/app.js | grep -i etag | awk '{print $2}' | tr -d '\r')
curl -I -H "If-None-Match: $ETAG" https://static.example.com/app.js | head -1
# 期望看到:HTTP/2 304
常见问题
Q:304 是错误吗?需要处理吗?
不是错误,是协商缓存命中的正常结果。通常不需要处理;如果发现静态资源频繁返回 304(每次都发请求协商),可以考虑给它们配长强缓存,让浏览器直接用本地副本,连请求都省掉。
Q:no-cache 和 no-store 有什么区别?
no-cache 允许缓存,但每次使用前必须向服务器确认(可能得到 304);no-store 是完全禁止缓存,每次都重新下载。想"改了就更新"用 no-cache,想"绝不落盘"用 no-store。
Q:为什么我改了 CSS,用户看到的还是旧的?
多半是强缓存 max-age 设得太长且文件名没变。最稳妥的做法是给静态资源文件名加内容指纹(如 app.3f8a.js),内容一变文件名就变,配合长强缓存,既快又不会看到旧文件。
Q:ETag 和 Last-Modified 同时存在,用哪个?
以 ETag 为准。浏览器会同时发送两个条件,服务器比较 ETag 决定是否 304。若多机环境 ETag 不一致造成反复重下,可考虑关闭 ETag 只用 Last-Modified,或统一 ETag 生成规则。
Q:调试时怎么避免 304 干扰?
DevTools 的 Network 面板勾选 "Disable cache"(需保持工具打开),或用 Ctrl+F5/Cmd+Shift+R 强制刷新,会跳过所有缓存看到真实响应;也可以用 curl 带 -H 'Cache-Control: no-cache' 观察。
总结
304 是协商缓存命中的正常状态,含义是"本地缓存仍有效,无需重新传输内容",它省的是响应体的流量,但请求本身仍要发生。
浏览器缓存分两级:强缓存(Cache-Control: max-age)命中时不发请求;强缓存失效后走协商缓存(ETag/Last-Modified),命中返回 304、未命中返回 200。
Last-Modified 基于时间、精度到秒、有误判风险;ETag 基于内容标识、更精确,但多机生成规则不一致会踩坑。两者同时在时以 ETag 为准。
Nginx 侧保持 etag on,用 expires 或 add_header Cache-Control 配置强缓存;带指纹的静态资源设长 max-age,HTML 用 no-cache 或短缓存。
别把 304 当错误;真正影响体验的是"该用强缓存却每次协商"和"该更新却因长缓存看到旧文件",前者靠长 max-age + 文件名指纹解决,后者靠缩短 HTML 缓存解决。
企业QQ咨询




