当浏览器再次请求一个已经访问过的静态资源时,抓包工具里经常能看到一个状态码为304的响应——服务器没有返回文件内容,浏览器却正确显示了资源。这就是HTTP协商缓存在工作。Nginx作为最常用的静态资源服务器,对这套机制的支持非常完善,理解它的内部原理,对于优化网站加载速度、减少带宽消耗都有直接帮助。这篇文章从HTTP缓存模型入手,把304的来龙去脉讲清楚。

一、HTTP缓存的两级模型:强缓存与协商缓存
要理解304,先要明白浏览器缓存是分层工作的。第一层是强缓存,由Cache-Control和Expires响应头控制。当强缓存有效时,浏览器直接从本地磁盘读取资源,根本不会向服务器发请求,开发者工具里会显示200 (from disk cache)或类似的提示。这一层完全不涉及Nginx的参与。
第二层才是协商缓存,也就是产生304的那一层。当强缓存过期后,浏览器会带着缓存标识信息(文件修改时间或内容指纹)去询问服务器:我手里的这个版本还能用吗?如果服务器判断资源没有变化,就返回304 Not Modified,响应体为空;如果变了,就返回200和完整的新内容。可以看到,304省掉的是响应体的传输,请求本身还是要发的。
两级缓存配合的逻辑是:强缓存追求零请求,协商缓存追求零传输。一个常见的策略是对带哈希指纹的资源(如app.a3f9c2.js)设置超长的强缓存时间,对不带指纹的资源(如index.html)关闭强缓存、只走协商缓存,这样既能保证更新及时,又能最大化命中缓存。
二、Last-Modified与If-Modified-Since:基于时间的校验
Nginx默认会对静态文件开启基于修改时间的协商缓存。第一次请求时,Nginx返回的响应头里会带上Last-Modified: Wed, 15 May 2024 08:30:00 GMT,这是文件在操作系统层面的mtime。浏览器把这个值记下来,下次请求时通过If-Modified-Since请求头原样带回。Nginx收到后与当前文件的mtime比较:如果一致,直接返回304,跳过文件内容发送;如果不一致,返回200和新文件。
这套机制的实现依赖Nginx的静态文件模块,判断逻辑非常轻量,只是字符串解析加时间比较,对服务器几乎没有额外开销。但它有一个先天缺陷:时间精度只有秒。如果文件在一秒内被修改了多次,mtime可能没变,浏览器会继续使用旧版本。另一个问题是被touch过的文件,内容没变但mtime变了,会导致一次无意义的重新下载。如果确实需要禁用这个头,可以在配置中关闭:
location /static/ {
# 关闭Last-Modified头的输出
last_modified off;
}一般来说不建议关闭,除非资源内容由上游动态生成,时间戳没有参考意义。还需要注意,If-Modified-Since只在GET或HEAD请求中有效,POST请求不会触发协商缓存,这是HTTP协议层面的规定,与Nginx无关。
三、ETag与If-None-Match:基于内容的校验
ETag是比Last-Modified更精确的校验方式,本质是资源内容的指纹。Nginx默认生成的ETag格式为"_mtime-文件大小",例如"6644e418-1a2b",前半部分是十六进制的修改时间戳,后半部分是十六进制的文件大小。浏览器第二次请求时通过If-None-Match头带上这个值,Nginx对比后决定返回304还是200。
当响应头里同时存在ETag和Last-Modified时,浏览器的协商顺序是先看If-None-Match。只有ETag匹配成功才返回304;如果ETag不匹配,即使If-Modified-Since能匹配上,也照样返回200。也就是说ETag的优先级更高,这解决了秒级精度的问题——只要内容或大小变了,指纹就会变。
使用ETag有一个经典的坑需要注意:多服务器部署时,如果开启了etag并且使用了inode相关信息,不同机器上的同一个文件会生成不同的ETag,导致缓存永远失效。不过从Nginx 1.3.3版本开始,默认的ETag格式已经不再包含inode,这个问题基本消失了。如果想显式控制,可以这样配置:
location /assets/ {
root /var/www;
# 开启ETag(默认已开启,通常无需显式写)
etag on;
# 强缓存1年,配合文件名哈希使用
add_header Cache-Control "public, max-age=31536000";
}四、典型场景配置与常见误区
来看一个前后端分离项目的经典配置。HTML入口文件需要及时更新,所以关闭强缓存,只依赖协商缓存;JS、CSS等带哈希指纹的静态资源则放心设置超长强缓存:
server {
listen 80;
server_name example.ipipp.com;
location = /index.html {
root /var/www/dist;
# 关闭强缓存,每次都协商
add_header Cache-Control "no-cache";
etag on;
}
location ~* \.(js|css|png|jpg|woff2)$ {
root /var/www/dist;
# 指纹资源缓存一年
expires 1y;
add_header Cache-Control "public, immutable";
}
}这里的no-cache经常被误解为不缓存,实际上它的含义是每次使用前必须向服务器协商验证,协商通过后依然可以复用本地缓存。真正不缓存的是no-store。而immutable是告诉浏览器,即使强缓存过期了也不要在刷新时发起协商请求,适合内容永不过期的指纹资源。
另一个误区是把304当成万能优化。304虽然省了响应体,但请求和响应头还是要传输,HTTP连接建立、TLS握手等开销依然存在。对于高频访问的小资源,304带来的节省很有限,更好的做法是通过强缓存或HTTP/2连接复用来减少请求本身。排查304问题时,可以先用curl验证:
# 第一次请求,拿到Last-Modified和ETag
curl -I http://example.ipipp.com/app.js
# 第二次请求,带上协商头观察是否返回304
curl -I -H "If-Modified-Since: Wed, 15 May 2024 08:30:00 GMT" \
-H 'If-None-Match: "6644e418-1a2b"' \
http://example.ipipp.com/app.js总结一下:304是Nginx与浏览器之间的一次缓存协商结果,核心机制是Last-Modified加If-Modified-Since的时间校验,以及ETag加If-None-Match的内容校验,两者都由Nginx静态文件模块自动处理。实际优化中,正确区分强缓存与协商缓存的适用对象,为指纹资源设置长缓存、为入口文件保留协商能力,才能把缓存体系的价值发挥到最大。
Nginx缓存304 Not Modified协商缓存修改时间:2026-09-06 00:18:52