
网站已经发布新版本,源站文件也确认更新,可有的用户仍看到旧页面。清理一次 CDN 缓存后,问题似乎消失了,第二天又有人反馈。遇到这种情况,先保留一个出问题的浏览器现场:访问的完整 URL、时间、响应头和页面版本。反复全站刷新缓存容易掩盖真正的原因。
本文适用于通过 CDN 提供 HTML、JavaScript 和 CSS 的网站,也涵盖注册了 Service Worker 的 PWA。示例域名与 IP 需要替换;命令用于诊断,不代表已经在你的生产环境实测。
先确认“旧”的到底是什么
旧 HTML、旧脚本和旧接口数据需要走不同的排查路径。建议在页面或构建产物中加入不含敏感信息的版本标记,例如构建提交号或发布时间,并记录 HTML 引用的脚本文件名。
如果 HTML 已经引用新脚本,界面仍显示旧数据,应继续检查接口响应和前端状态。若 HTML 还在引用上一版的文件,则先查 HTML 的传递链路。对同一个文件反复覆盖内容,却保持 URL 不变,会让版本判断变得困难。
先向用户收集以下信息:
- 完整 URL,包括查询参数;登录与未登录是否一致。
- 正常与异常用户各自看到的构建版本。
- 浏览器 Network 面板中的请求状态、响应头,以及是否显示来自内存、磁盘或 Service Worker。
- 是否只在某个地区、设备或某个仍然打开的旧标签页中发生。
不要把带 Cookie、Authorization 或签名参数的完整请求贴到公开工单里。
用同一个 URL 对比浏览器、CDN 与源站
先在普通终端获取公开页面。下面使用 GET 丢弃正文,避免 HEAD 与 GET 处理差异影响判断:
curl -sS -D - -o /dev/null https://www.example.com/
重点记录 Cache-Control、Age、ETag、Last-Modified、Vary,以及供应商提供的缓存状态头。Age 可以帮助判断共享缓存响应的年龄,但它不能单独证明响应内容是否正确;没有 Age 也不能证明请求没有经过缓存。缓存命中头的名称和语义以服务商文档为准。
需要核对正文时,另外保存文件并比较版本标记:
curl -sS -D edge-headers.txt -o edge.html https://www.example.com/
# 仅在拥有源站访问权限且源站允许该连接时执行
curl -sS --resolve www.example.com:443:203.0.113.10 \
-D origin-headers.txt -o origin.html https://www.example.com/
203.0.113.10 是文档示例地址。--resolve 保持 URL 中的主机名、TLS SNI 与证书校验名称,同时指定连接 IP。不要为了得到一个“成功”结果加上 -k。如果源站仅接受 CDN 网络流量,应使用获准的运维入口,勿临时向公网放开防火墙。
源站自身也可能存在反向代理或应用缓存,因此定向源站得到旧内容时,还要继续核对实际部署目录、负载均衡后的各实例以及应用缓存。一个实例更新成功,不能说明整个源站池已经一致。
可以增加一次要求重新验证的对照请求:
curl -sS -H 'Cache-Control: no-cache' -D - -o /dev/null \
https://www.example.com/
这个请求头表达重新验证的意图,不是清空 CDN 的指令。CDN 是否转发、遵循或覆盖它取决于配置。临时追加随机查询参数也不能作为可靠的绕缓存方法:查询参数可能被缓存键忽略,还可能制造大量额外缓存对象。
浏览器缓存:分清重新验证与禁止存储
对需要及时更新的公共 HTML,Cache-Control: no-cache 是常见选择。它允许缓存保存响应,但再次使用前需要成功验证。no-store 才是要求不存储响应,不能把两者混为一谈。[1]
例如,公共页面可以考虑以下思路,但必须结合 CDN 规则核对最终响应:
Cache-Control: no-cache
ETag: "html-build-example"
ETag 应由服务器根据实际内容生成,上面的值仅说明格式。重新验证返回 304 Not Modified 后,浏览器继续使用本地正文,这是正常行为。若文件内容已改变而验证标记保持不变,错误的 304 就可能让旧内容继续出现。检查是否有固定 ETag、异常的修改时间,或不同源站实例返回不一致的标记。
对带内容指纹的静态资源,例如 app.a81d9f.js,可采用较长缓存期:
Cache-Control: public, max-age=31536000, immutable
前提是 URL 与内容绑定,文件改变时使用新 URL。不要把这个策略直接套给持续覆盖的 app.js 或 HTML。涉及个人数据的页面还需要单独设计私有缓存与存储策略,不能直接照搬公共页面规则。
修改服务器响应头,不会主动召回浏览器已经保存、且仍处于新鲜期内的旧响应。若历史上给 HTML 设置了很长的有效期,修复可能需要等待、提供受控迁移路径或引导受影响用户做定向处理。一次 CDN 刷新无法清掉每个用户的本地缓存。
CDN 节点:核对实际缓存键与规则优先级
当源站是新版本、终端请求 CDN 仍拿到旧版本时,查看该 URL 对应的缓存规则与刷新记录。不要只看“刷新成功”的提示,还应确认刷新范围是否与实际对象匹配。
常见遗漏包括:HTTP 与 HTTPS、不同主机名、路径末尾斜杠、查询参数,以及移动端或语言版本。哪些差异构成不同对象,取决于实际缓存键。若站点使用 Cookie、请求头或查询参数决定页面内容,也需要核对相应维度是否进入缓存键,或该响应是否应绕过共享缓存。
再检查是否有边缘规则覆盖源站的 Cache-Control,以及错误期间是否允许提供过期内容。stale-while-revalidate 允许在规定窗口内先返回旧响应并后台更新;stale-if-error 可以在符合条件的错误发生时提供过期响应。它们有明确用途,但不能用来解释无限期的旧内容。[1]
修复时先对一个已确认的 URL 做精准刷新,检查响应正文与版本,再扩展到必要范围。全站刷新会增加回源压力;业务高峰前应评估源站容量,并准备停止批量刷新或恢复原规则的方案。
Service Worker:浏览器可以绕过你以为的网络路径
如果终端获取到新页面,某台浏览器却一直拿到旧版本,打开开发者工具的 Application 面板,查看当前域名是否有 Service Worker 注册,以及 Cache Storage 中保存的对象。
Service Worker 的 fetch 处理逻辑可以直接返回 Cache Storage 中的响应。即使 HTTP 缓存头已经改为 no-cache,也不能保证自定义的缓存优先逻辑会重新访问网络。需要分别排查 HTTP 缓存与 Cache Storage。[2]
新 Service Worker 下载完成后,也可能仍处于 waiting 状态,等待旧 worker 所控制的页面退出。调用注册对象的 update() 会触发更新检查,但不保证立刻接管所有页面。skipWaiting() 与 clients.claim() 会改变接管时机,使用前要确认新旧页面与资源能够兼容;仓促接管可能使旧 HTML 与新脚本混用。[2][3]
仅在测试账号、测试浏览器或用户明确同意的情况下,定向注销 Service Worker 或清理该站点数据作为诊断。清理可能删除离线页面、未同步草稿或本地登录相关状态,不能把“清空所有站点数据”当成无风险的通用修复。
长期修复应落在应用代码中:为缓存命名和升级制定规则,在合适的激活阶段清理本应用的旧缓存;HTML 可按业务选择网络优先并提供离线回退;静态资源使用指纹 URL。不要删除同源下其他应用仍在使用的缓存。
发布顺序与验收:别让新页面引用尚未就绪的文件
上线前保留当前构建产物、CDN 规则和 Service Worker 版本。通常先上传新指纹资源,确认源站与 CDN 都能获取,再发布引用它们的 HTML。旧资源要保留一段与访问场景匹配的过渡期,供旧标签页、旧 worker 和回滚使用。具体保留时间应根据流量、缓存期与业务要求决定。
验收至少覆盖:
- 源站与 CDN 返回的 HTML 构建版本一致,引用的资源全部可访问。
- 新开浏览器会话能够取得新版本,原有标签页按预定策略更新。
- Service Worker 的 active、waiting 状态符合预期,离线回退仍正常。
- 登录用户与匿名用户没有串用缓存内容,不同地区反馈也得到复查。
若新版本异常,恢复上一版 HTML 及兼容资源,并按原计划处理 CDN 与 worker 缓存。回滚文件之后也要重新验证终端实际收到的版本。
最终应能回答一个具体问题:旧内容由哪一层返回,依据是什么?把异常请求、响应头、构建版本与修复后的对照结果留在工单中。下次出现相似反馈时,就不必从“再刷一次缓存”重新开始。
参考资料
资料核验日期:2026 年 10 月 20 日。示例需按实际 CDN 与应用配置调整。