为什么网站明明没有改,浏览器还要重新下载资源?
打开一个网页时,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 共同组成的网站,真正重要的是明确每一层缓存的职责。遇到旧内容时逐层定位,而不是简单关闭缓存,通常才能同时获得正确性和性能。