部署成功但页面还是老内容:3 个原因 + 修复路径

Vercel/Netlify 仪表盘绿勾、commit hash 也对,访客刷新还是昨天那版——多半是 service worker 抢答、CDN 没失效或中间代理缓存。本文给一条 10 分钟诊断路径。

Vercel / Netlify / Firebase 仪表盘绿勾打钩、commit hash 也对得上,但访客刷新还是看到昨天那版——这是部署管线里最让人怀疑人生的一类问题。原因几乎一定在”内容已经到边缘节点了,但请求没有走到新版本”这条链上:service worker 抢答、CDN edge 没失效、HTML 被中间代理缓存。本文按命中率排出 5 类常见来源,并给一条 10 分钟以内可以打完的诊断路径。

常见原因

按命中率从高到低。

1. Service Worker 抢在网络前返回了旧 HTML

PWA 模板(Workbox / Next PWA / Vite PWA)默认会注册一个 service worker 缓存 HTML 和静态资源。worker 在用户上一次访问时已经把旧版 index.html 缓存进 Cache Storage,下次刷新它直接 respondWith(cache),根本没有发起新请求。

// 典型的"先缓存后网络"策略,最容易卡住
self.addEventListener("fetch", e => {
  e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
});

如何判断:DevTools → Application → Service Workers,看到 “Activated and is running”;Network 面板里页面请求显示 (ServiceWorker) 作为来源。

2. CDN edge cache 没有失效

Vercel / Cloudflare / Firebase 都会在边缘节点缓存 HTML(默认 TTL 因 framework / 路由而异)。如果你只重新部署而没有触发 invalidation——例如把构建产物直接传到 S3、或者改了 origin 但没调用 purge API——edge 还在返回上一版。

如何判断curl -I https://yourdomain.com/your-page 看到 x-vercel-cache: HIT / cf-cache-status: HIT / age: 3600 这种命中标记。

3. 浏览器 HTML 被 long-cache

如果你(或者历史上某次部署)把 HTML 的 Cache-Control 设成 public, max-age=31536000,浏览器一年内根本不会回源。Vercel 默认 HTML 是 s-maxage=0, must-revalidate,但自定义 headers 或 framework 配置(比如 Next.js 的 headers())可能误覆盖。

如何判断:DevTools → Network → 点 HTML 请求 → Response Headers 看 Cache-Control;如果出现 max-age= 大于 0 就有嫌疑。

4. 部署成功但用的是旧的 commit / build

CI 触发了部署,但 build step 用了缓存的 dist/,或者你 push 到了错误的分支。Vercel “Production” 默认绑定 main 分支,push 到 feature 分支只会生成 preview,控制台仍然显示”Ready”。

如何判断:查看部署详情里的 commit SHA,和你本地 git rev-parse HEAD 对比;不一致就是构建用的不是最新代码。

5. 域名指到旧 origin / 多个 origin

域名同时挂在 Vercel 和 Cloudflare Pages 上,或者历史上把 A 记录指向旧服务器从未清理。新部署在 Vercel 上,但 DNS 还把一部分流量带到旧 origin。

如何判断dig +short yourdomain.com 拿到 IP 后反查归属(whois),看是不是当前宿主的 IP 段。

最短修复路径

按”先排除客户端、再排除 edge、最后查 origin”的顺序。前 2 步通常就解决了。

Step 1:用无痕窗口隔离客户端缓存

开一个 incognito 窗口,DevTools → Network 勾上 “Disable cache”,访问页面。

看到的版本结论
新版本普通窗口里的浏览器 / SW 缓存问题 → 进 Step 2
还是老版本不是浏览器缓存,是 edge 或 origin → 跳到 Step 3

普通窗口里硬刷一次(Cmd/Ctrl+Shift+R)也可以一并验证。

Step 2:卸载 service worker + 清站点数据

DevTools → Application → Service Workers → 每个注册项点 “Unregister”。然后 Application → Storage → 勾全 → “Clear site data”,刷新页面。

如果想一键脚本化清掉所有用户那边的 SW(部署一次新 worker 时执行),可以临时发一个”自杀 worker”:

// public/sw.js(部署一次,等老用户上来命中,自动注销)
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", async () => {
  const keys = await caches.keys();
  await Promise.all(keys.map(k => caches.delete(k)));
  const regs = await self.registration.unregister();
  const clients = await self.clients.matchAll();
  clients.forEach(c => c.navigate(c.url));
});

Step 3:手动 purge edge cache

按宿主选一项:

# Vercel — 重新部署时跳过 build cache
vercel --prod --force

# Cloudflare — 仪表盘 Caching → Configuration → Purge Everything
#   或用 API:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

# Firebase Hosting — 重新部署即可触发 CDN 失效
firebase deploy --only hosting --force

清完之后用 curl -I 确认:

curl -sI https://yourdomain.com/your-page | grep -iE 'cache|age'
# 期望: x-vercel-cache: MISS  或  cf-cache-status: MISS  且 age: 0

Step 4:核对部署用的 commit 是不是最新

git rev-parse HEAD
# 然后在 Vercel / Netlify 部署详情页对比 "Source" 那一行的 SHA

不一致就重新 push 一次、或在仪表盘点 “Redeploy” 并取消 “Use existing Build Cache”。

Step 5:确认 DNS 没指到旧 origin

dig +short yourdomain.com
# 把返回 IP 复制到 https://ipinfo.io/ 看 org,是不是当前宿主

如果发现还残留旧 A 记录(比如 GitHub Pages 的 185.199.x.x),删掉只保留宿主推荐的那条。

预防建议

  • 资源文件名带 content hash(Vite / Next / Astro 默认都开),HTML 始终走短 TTL(s-maxage=0, must-revalidate)。
  • service worker 用 “network-first for navigation, cache-first for hashed assets” 策略,并在 install 阶段 self.skipWaiting()
  • 每次发版后把 commit SHA 写进一个 /version.json,前端启动时对比并提示用户刷新。
  • 在 CI 里加 smoke test:部署后 curl 首页,断言响应里包含本次 commit SHA。
  • 一个域名只挂一个 origin;历史 A / CNAME 记录用 dig 季度审一次。

相关阅读

标签: #部署 / 托管 #排查 #排查