首页
首页 / 技术教程

HTTP 缓存:Cache-Control、ETag 与协商缓存

技术教程 网络 / HTTP / DNS
预计阅读 12 分钟·最后更新 2026-10-21

为什么网站明明没有改,浏览器还要重新下载资源?

打开一个网页时,HTML、CSS、JavaScript、图片和字体都可能产生 HTTP 请求。如果用户每刷新一次页面,浏览器都重新下载所有资源,不仅浪费带宽,也会增加服务器压力和页面等待时间。

HTTP 缓存解决的核心问题就是:已经拿到过的资源,下一次还能不能继续使用?

实际排查缓存问题时,最重要的不是背诵响应头,而是先区分两件事:

  • 浏览器能否直接使用本地副本,不访问服务器。
  • 浏览器需要询问服务器资源是否变化,再决定是否继续使用本地副本。

前者通常与强缓存有关,后者就是常说的协商缓存。

先用一次真实请求理解 HTTP 缓存

假设浏览器第一次访问:

GET /assets/app.css HTTP/1.1
Host: example.com

服务器返回:

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400
ETag: “css-v18”
Content-Type: text/css

浏览器不仅获得了 CSS 内容,同时还获得了缓存规则。max-age=86400 表示在符合条件的情况下,这份响应可以保持新鲜 86400 秒,也就是 24 小时。

如果用户几分钟后再次访问同一资源,浏览器可能直接使用缓存,而不需要重新下载完整 CSS。

Cache-Control 到底控制什么?

Cache-Control 是现代 HTTP 缓存中最重要的响应头之一。它可以告诉浏览器和中间缓存:资源是否允许缓存、可以缓存多久,以及重新验证时应该如何处理。

max-age:资源可以保持新鲜多久

Cache-Control: max-age=3600

表示该响应在 3600 秒内可以被视为新鲜。对于浏览器而言,在缓存仍然新鲜并满足其他条件时,可以避免重新获取完整资源。

public:允许共享缓存保存

Cache-Control: public, max-age=86400

public 表示响应可以被共享缓存保存,例如某些 CDN 或代理缓存。对于公开的静态 CSS、JS、图片等资源很常见。

private:主要留给单个用户的缓存

Cache-Control: private, max-age=300

private 通常用于不应该被共享缓存存储的个性化响应。浏览器仍可能缓存,但共享代理不应把它作为公共内容提供给其他用户。

no-store:不要保存响应

Cache-Control: no-store

no-store 的含义很直接:缓存不应存储该响应。对于高度敏感、确实不希望被缓存的数据可以使用,但不应该为了“保证最新”就在全站滥用。

no-cache 并不是完全不缓存

Cache-Control: no-cache

这是最常被误解的指令之一。no-cache 并不等同于 no-store。它允许响应被保存,但在重新使用前通常必须向服务器验证。

什么是强缓存?

所谓“强缓存”并不是 HTTP 规范中的一个独立协议名称,而是前端开发中常用的描述:当缓存仍然新鲜时,浏览器可以直接使用已有响应,而无需为了确认资源是否变化而重新请求服务器。

例如:

Cache-Control: public, max-age=31536000

如果静态文件 URL 长期不变,这条规则可能导致浏览器在很长时间内继续使用旧文件。因此长缓存通常要和文件版本化一起使用。

/assets/app.8f21c3.js
/assets/style.19ab72.css

内容变化时生成新的文件名或 hash,URL 随之改变。这样既可以放心设置长缓存,又能在发布新版本时让浏览器请求新的资源。

什么是协商缓存?

当缓存已经过期,或者策略要求重新验证时,浏览器不一定需要重新下载整个文件。它可以携带验证信息询问服务器:我手里的这份资源还能继续用吗?

如果资源没有变化,服务器可以返回 304 Not Modified。

GET /assets/app.css HTTP/1.1
If-None-Match: “css-v18”

服务器判断 ETag 仍然匹配后返回:

HTTP/1.1 304 Not Modified
ETag: “css-v18”

304 响应通常不需要再次传输完整资源正文,浏览器继续使用本地已有副本。

这就是协商缓存的核心价值:仍然访问服务器进行验证,但避免无意义地重复传输完整文件。

ETag 是怎么工作的?

ETag 是服务器为某个资源版本提供的实体标签。浏览器第一次获得资源时,可以保存服务器返回的 ETag。

ETag: “css-v18”

之后重新验证时,浏览器可以发送:

If-None-Match: “css-v18”

服务器比较当前资源的 ETag:

  • 没有变化:通常返回 304 Not Modified。
  • 已经变化:返回新的 200 响应以及更新后的内容和 ETag。

ETag 的优势是可以围绕资源版本进行验证,而不必单纯依赖修改时间。

Last-Modified 和 ETag 有什么区别?

除了 ETag,HTTP 还可以使用 Last-Modified 与 If-Modified-Since 进行条件请求。

Last-Modified: Tue, 20 Oct 2026 08:30:00 GMT

浏览器之后可能发送:

If-Modified-Since: Tue, 20 Oct 2026 08:30:00 GMT

两者都可以用于判断资源是否变化,但机制不同。

  • Last-Modified 基于资源修改时间。
  • ETag 基于服务器提供的资源实体标签。
  • ETag 可以表达比时间戳更细的资源版本差异。
  • 具体优先级和行为应以服务器实现及 HTTP 条件请求规则为准。

实际网站中两者同时出现很常见,没有必要为了“只留一个”而随意删除服务器默认配置。

200、304 和 memory cache / disk cache 有什么区别?

调试浏览器缓存时,经常会在开发者工具里看到不同结果。

200

通常意味着服务器返回了成功响应以及资源内容。但是否来自网络、Service Worker 或其他机制,还应结合浏览器开发者工具的详细信息判断。

304 Not Modified

表示客户端进行了条件请求,服务器确认缓存副本仍然可用,因此不需要重新发送完整正文。

memory cache

浏览器直接从内存缓存中获取资源。它通常非常快,但生命周期和具体行为由浏览器决定。

disk cache

浏览器从磁盘缓存读取资源,不需要重新下载完整文件。

因此看到 304 并不代表“缓存失败”。相反,它说明协商缓存正在发挥作用。

静态资源应该怎么设置缓存?

对于带内容 hash 的 CSS、JavaScript、字体和图片,可以考虑使用很长的缓存时间。

Cache-Control: public, max-age=31536000, immutable

前提是:文件内容改变时 URL 也会改变。

例如:

app.a91f03.js  →  发布新版本  →  app.c28e51.js

这种策略可以兼顾缓存命中率和版本更新。

如果文件名永远是 app.js,却设置一年缓存,那么发布新代码后就可能出现用户仍然拿到旧资源的问题。

HTML 页面也应该缓存一年吗?

通常不建议直接照搬静态资源策略。

HTML 往往承担入口文档的角色,其中会引用最新的 CSS、JavaScript 和其他资源。如果 HTML 长时间保持旧版本,就可能继续引用已经过期的资源地址。

因此 HTML、API、静态资源应该根据内容性质分别设计缓存策略,而不是全站统一设置一个 max-age。

WordPress 网站缓存时特别要注意什么?

WordPress 站点通常同时存在多层缓存:

  • 浏览器缓存
  • Nginx 或 Apache 缓存
  • WordPress 页面缓存插件
  • 对象缓存
  • CDN 缓存

因此出现“后台已经更新,前台还是旧内容”时,不应该第一反应就是关闭所有 HTTP 缓存。

更合理的排查顺序是:

  • 确认 WordPress 页面本身是否已经更新。
  • 检查页面缓存插件是否仍保存旧页面。
  • 检查服务器缓存是否命中旧版本。
  • 检查 CDN 是否需要刷新对应 URL。
  • 最后再检查浏览器缓存和 Cache-Control。

多层缓存最大的难点不是“有没有缓存”,而是必须知道旧内容究竟停留在哪一层。

如何用浏览器检查 Cache-Control 和 ETag?

打开 Chrome 或 Edge 开发者工具,进入 Network 面板,然后重新加载页面。

点击某个 CSS、JS、图片或 HTML 请求,在 Headers 中重点检查:

  • Cache-Control
  • ETag
  • Last-Modified
  • Age
  • Expires
  • If-None-Match
  • If-Modified-Since

同时观察 Status 和 Size/Transferred 等信息,可以帮助判断资源是重新下载、协商验证还是直接从本地缓存读取。

调试时要注意:开发者工具中的“Disable cache”会改变正常缓存行为。测试真实用户缓存效果时不要一直开启它。

如何用 curl 检查缓存响应头?

可以先查看响应头:

curl -I https://example.com/assets/app.css

重点观察:

cache-control:
etag:
last-modified:
age:

如果要测试 ETag 条件请求,可以先取得 ETag,再发送 If-None-Match。示意:

curl -I -H ‘If-None-Match: “css-v18″‘ https://example.com/assets/app.css

如果资源没有变化,并且服务器正确支持相应条件请求,可能得到 304。

为什么修改网站后用户仍然看到旧页面?

这个问题不一定是浏览器缓存造成的。

常见原因包括:

  • CDN 节点仍然缓存旧页面。
  • WordPress 缓存插件没有正确清理。
  • Nginx FastCGI Cache 等服务器缓存仍然命中。
  • 静态资源文件名没有版本变化,却设置了很长的 max-age。
  • Service Worker 保存了旧资源。
  • 浏览器本地缓存仍然有效。

正确做法是逐层判断响应究竟来自哪里,而不是让用户长期依赖 Ctrl+F5 强制刷新。

常见错误配置

错误一:把 no-cache 当成完全禁止缓存

no-cache 与 no-store 不是同一个意思。需要完全避免存储时才应考虑 no-store。

错误二:所有资源都设置一年缓存

长期缓存更适合 URL 随内容变化的静态资源。HTML 和动态 API 不应该机械照搬。

错误三:文件内容变了,URL 永远不变

这会让长缓存资源的版本更新变得困难。静态资源最好结合 hash 或版本号。

错误四:看到 304 就认为网站很慢

304 是条件请求正常工作的结果。是否存在性能问题应该结合 RTT、TTFB、资源数量和真实加载时间判断。

错误五:WordPress 更新后只清浏览器缓存

如果真正的旧内容在 CDN、页面缓存或服务器缓存层,清浏览器并不能解决根本问题。

一套更实用的缓存思路

可以把网站资源按性质分开处理,而不是寻找一条适用于全站的万能 Cache-Control。

  • 带 hash 的 CSS / JS / 字体 / 图片:适合长时间缓存,并通过新 URL 发布新版本。
  • HTML:通常采用更谨慎的缓存策略,确保入口页面能够及时引用最新资源。
  • 公开 API:根据数据更新频率决定是否允许缓存以及缓存多久。
  • 用户私有页面:谨慎使用共享缓存,避免个性化数据被错误复用。
  • 敏感响应:根据安全需求决定是否使用 no-store。

缓存策略最终应该服务于两个目标:减少不必要的网络传输,同时保证用户能够及时获得正确的新版本。

总结

理解 HTTP 缓存时,不需要把所有响应头死记硬背。先判断资源处于哪一种状态会更容易:

  • 缓存仍然新鲜:浏览器可能直接复用已有响应。
  • 缓存需要验证:使用 ETag / Last-Modified 等条件请求与服务器确认。
  • 资源没有变化:服务器可以返回 304,继续使用本地副本。
  • 资源已经变化:服务器返回新的内容,并更新缓存信息。

Cache-Control 决定缓存如何被使用,ETag 和 Last-Modified 可以帮助完成重新验证,而文件版本化则让静态资源能够安全使用更长的缓存时间。

对于 WordPress、CDN 和 Nginx 共同组成的网站,真正重要的是明确每一层缓存的职责。遇到旧内容时逐层定位,而不是简单关闭缓存,通常才能同时获得正确性和性能。

发表评论