tech
HTTP 缓存:从 Expires 到 ETag
梳理浏览器缓存的目标、强缓存与协商缓存,以及生产环境中静态资源和 HTML 的缓存策略。
缓存的核心很简单:浏览器或中间代理保存服务器返回的资源,下次请求时优先使用已经保存的数据。
这样做主要解决三个问题:
- 减少网络延迟。一次请求可能需要几十到几百毫秒,命中缓存后可以直接从浏览器缓存读取。
- 减少服务器压力。相同资源不需要反复生成和传输。
- 节省带宽。静态资源不必在每次访问时重新下载。
Expires:依赖客户端时间
HTTP/1.0 时代通常使用 Expires:
Expires: Wed, 30 Jul 2026 10:00:00 GMT
浏览器将当前时间与 Expires 比较,判断资源是否过期。问题在于,当前时间来自客户端自己的系统时钟。用户可以手动修改系统时间,因此缓存判断可能出现偏差。
Cache-Control:由浏览器记录缓存时间
HTTP/1.1 引入了更常用的 Cache-Control:
Cache-Control: max-age=3600
max-age=3600 表示资源在缓存后 3600 秒内有效。浏览器自己记录资源写入缓存的时间,不再依赖服务器下发的绝对时间,也就避免了客户端系统时间造成的部分问题。
强缓存与协商缓存
如果缓存过期后每次都重新下载资源,大量请求仍然会同时回到服务器。因此 HTTP 缓存通常分为两种行为。
强缓存
在有效期内,浏览器直接使用本地缓存,不向服务器发送请求。常见配置是:
Cache-Control: public, max-age=3600
资源过期后,浏览器才会重新发起请求。
协商缓存
缓存过期后,浏览器先询问服务器资源是否发生变化:
- 资源没有变化,服务器返回
304 Not Modified,浏览器继续使用本地缓存。 - 资源发生变化,服务器返回
200和新的资源内容。
协商缓存可以避免重复传输完整文件,但仍然需要一次请求和响应。
Last-Modified 的局限
服务器可以通过 Last-Modified 告诉浏览器资源的最后修改时间:
Last-Modified: Wed, 30 Jul 2026 10:00:00 GMT
下一次请求时,浏览器发送:
If-Modified-Since: Wed, 30 Jul 2026 10:00:00 GMT
服务器比较文件修改时间后决定返回 304 还是新内容。
它的缺点是时间精度通常只能到秒。假设资源在 10:00:00.500 发生变化,而服务器记录仍然是 10:00:00,就可能误判资源没有变化。
ETag:使用内容指纹
ETag 用资源内容生成一个指纹。只要内容发生变化,指纹就应该变化:
ETag: "abc123"
浏览器下次请求时发送:
If-None-Match: "abc123"
服务器比较当前资源的 ETag:
- 相同:返回
304 Not Modified。 - 不同:返回
200和新的资源内容。
因此,生产环境通常会组合使用:
Cache-Control: max-age=3600
ETag: "资源内容指纹"
Nginx 配置示例
HTML 入口文件通常不做很长时间的强缓存,让浏览器通过协商缓存及时检查新版本:
server {
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache";
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
带哈希文件名的静态资源,例如 app.abc123.js 和 style.def456.css,内容一旦变化文件名也会变化,因此适合缓存一年并设置 immutable。HTML 入口文件则保留较短的缓存策略,避免用户长期拿到旧版本入口。
Nginx 默认会开启 ETag(etag on),也可以根据项目需要显式配置。
实际选择
可以先记住一条简单规则:
- HTML:短缓存或
no-cache,使用协商缓存检查版本。 - 带内容哈希的 JS、CSS、字体和图片:
public, max-age=31536000, immutable。 - 没有哈希的静态资源:使用较短的
max-age,并配合 ETag。
缓存策略不是越久越好,关键是资源 URL 是否能在内容变化时同步变化。只要文件名带有可靠的内容指纹,长缓存才不会妨碍发布新版本。
