Firebase 缓存:部署后强制刷新页面

部署完用户还看到旧页?用这份 firebase.json 缓存头配置 + curl 诊断 + 正确的回滚命令(2026 年 6 月已验证)几秒搞定。

你部署了。*.web.app 上新版本已经在跑。朋友打开自定义域名,看到的还是昨天的标题。这几乎都是缓存问题——但反直觉的是,元凶通常不是 Firebase 的 CDN。一次 firebase deploy --only hosting 已经把整个 CDN 边缘缓存清掉了。还在显示旧页的,是访客浏览器里那份副本,或者一个陈旧的 service worker。修法是在 firebase.json 里配好 Cache-Control,让浏览器去 revalidate,而不是傻信本地旧副本。

一句话总结

  • Firebase Hosting 每次部署都会自动清空 CDN 缓存。官方文档原话:Firebase Hosting “automatically clears all your cached content across the CDN until the next request”。
  • 你没显式设 Cache-Control 的文件,默认拿到 max-age=3600(正好 1 小时)。这一小时就是部署后 HTML 看起来还旧的原因。
  • firebase.json 里把 HTML 设成 no-cache, max-age=0,把带内容 hash 的资源(Astro 的 /_astro/**、Vite 产物)设成 max-age=31536000, immutable
  • 部署后用 curl -sI 验证——别用 DevTools,它自己还有一层缓存。浏览器硬刷一次就能清掉那份卡住的本地副本。

真正缓存你页面的是谁

三层都可能压着旧页。分清是哪一层,决定了这是 10 秒的修复还是一小时的瞎折腾。

部署时会清吗?靠什么清
Firebase CDN 边缘会,每次部署自动清firebase deploy --only hosting
浏览器 HTTP 缓存不会你设的 Cache-Control 头(no-cache 强制 revalidate)
Service worker 缓存不会SW 里的版本号递增 + skipWaiting()

任何没显式设 Cache-Control 的文件,默认是 public, max-age=3600——1 小时(对照 Firebase 文档,2026 年 6 月)。对图片和带 hash 的 JS/CSS 没问题,但放在 HTML 上就意味着部署成功后访客最长还能拿到一小时的旧页。另外 Firebase 默认在边缘缓存 404 响应 10 分钟,所以你刚加的路由上线后可能会短暂 404。

判断标准

  • 部署成功了,新内容只有无痕窗口能看到。
  • 手机看到新的、电脑看到旧的(或反过来)——浏览器缓存状态不同。
  • 自定义域名是旧内容,*.web.app 是新——边缘缓存按 host 区分。
  • 老的 service worker 一直返回缓存里的旧资源。
  • curl -I 看 HTML URL 没有 cache-control 头(用了默认)。

开始前准备

  • 构建输出目录要和 firebase.json 对得上(Astro 是 dist,Next 静态导出是 out)。
  • 装好 curl 验证响应头——不要只看 devtools,它自己也有缓存行为。
  • 有 service worker 的话先找到源文件,再改缓存。

实操步骤

  1. 先诊断。 看慢路径上的缓存状态:
curl -sI https://yourdomain.com/ | grep -iE 'cache-control|age|x-cache'
# 典型坏态:
#   age: 1840
#   x-cache: HIT
#   (没有 cache-control 或写着 'public, max-age=3600')
  1. firebase.json——生产可用完整配置:
{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "cleanUrls": true,
    "trailingSlash": true,
    "headers": [
      {
        "source": "**/*.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache, max-age=0" }
        ]
      },
      {
        "source": "/_astro/**",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "**/*.@(js|css|woff2)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "**/*.@(jpg|jpeg|png|webp|avif|svg)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=2592000" }
        ]
      },
      {
        "source": "/sitemap*.xml",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=3600" }
        ]
      }
    ]
  }
}
  1. HTML 用 no-cache, max-age=0,不是 no-store no-cache 是”存一份但每次都 revalidate”,浏览器还能用本地副本,只是每次都查新鲜度。no-store 强制完整重新下载,浪费。

  2. 带 hash 的静态资源加 immutable 只有文件名随内容变才能用。Astro 的 /_astro/** 和 Vite 的 hashed 输出都符合;平常的 app.js 不符合。

  3. 部署后验证边缘头:

npm run build
firebase deploy --only hosting

# 30 秒后:
curl -sI https://yourdomain.com/                          | grep -i cache-control
# cache-control: no-cache, max-age=0

curl -sI https://yourdomain.com/_astro/index.abc123.css   | grep -i cache-control
# cache-control: public, max-age=31536000, immutable
  1. **浏览器卡住的话,硬刷一次(Cmd+Shift+R / Ctrl+Shift+R)**清本地缓存。之后 no-cache 头就会保证一直是新的。

  2. 有 service worker 的,每次部署 ship 新版本 SW 并 skipWaiting()——否则陈旧缓存能存一周:

// public/sw.js
const VERSION = 'v2026-05-22-1';                 // 每次部署递增

self.addEventListener('install', (e) => {
  self.skipWaiting();                            // 立刻激活
});

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => !k.includes(VERSION)).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});
  1. 如果上线了一个有问题的版本,回滚到已知正常的版本。 注意:没有 firebase hosting:rollback 这个 CLI 命令(2026 年 6 月)——回滚要么点控制台按钮,要么按版本 ID 克隆。先列版本,找到 VERSION_ID,再把它克隆到 live 频道:
firebase hosting:versions:list
# 把某个旧版本(注意 @VERSION_ID 写法)克隆到 live:
firebase hosting:clone YOURPROJECT:@VERSION_ID YOURPROJECT:live

多数人更省事的路子:Firebase 控制台 → Hosting → 发布历史(Release history)→ 悬停那个好版本 → 菜单 → Roll back(回滚)。不管哪种方式,回滚本身也是一次部署,所以会再次自动清空 CDN 边缘缓存。

执行检查清单

  • HTML、hashed 资源、图片、sitemap 在 firebase.json 里都有显式 Cache-Control
  • 每次部署后 curl -sI 验证响应头。
  • service worker(如果有)每次部署递增版本号 + skipWaiting()
  • build 产物的 /_astro/** 或等价目录文件名带 hash。

上线后验证

  • 用不同网络(或 WebPageTest 这种远程工具)curl -sI——确认边缘缓存确实更新了,不是只你本地节点。
  • 无痕 + 未登录浏览器都立刻看到新版本。
  • DevTools → Application → Service Workers 显示新版本已激活。

容易踩的坑

  • HTML 留默认 max-age=3600,每次部署后追”残影”最长能追一小时。
  • 把锅甩给 Firebase CDN。部署已经清掉它了。页面还旧,说明旧副本在浏览器或 service worker 里。
  • HTML 上设 immutable——浏览器永远不再 revalidate,硬刷都没用。
  • 框架适配器里设的 header 和 firebase.json 冲突——匹配到的路径以 firebase.jsonheaders 为准;用 curl -sI 看实际响应确认。
  • service worker 有 bug,又没递增版本号,部署后永远返回旧 bundle。
  • 本想用 no-cache 却写成 no-store——no-store 把浏览器的前进/后退缓存也关了,白白拖慢导航,这里没好处。

FAQ

  • 怎么让所有人立刻刷新?: 重新部署。这会自动清空整个 CDN 边缘缓存。浏览器里还压着长缓存副本的访客,下次请求就会 revalidate——这正是 HTML 上 no-cache 保证的。
  • 部署时 Firebase 会清 CDN 缓存吗?: 会,而且清的是全部文件,不只是改动过的。文档原话是 Firebase “automatically clears all your cached content across the CDN until the next request”。所以持续陈旧是浏览器或 service worker 的问题,不是 CDN。
  • 不同路径能设不同缓存吗?: 能。headers 数组支持多个 source glob 模式,每个独立配。两个模式都匹配同一个文件时,数组里排在前面的那块对该 header 生效。
  • 页面新了 CSS 还旧——为啥?: CSS 文件名多半没带内容 hash,那它那个 immutable, max-age=31536000 头就在撒谎。要么用构建工具给文件名加 hash(Astro、Vite 默认就会),要么先把这个文件降到短 max-age
  • no-cache 会损性能吗?: 对 HTML 几乎不损。浏览器留着本地副本,用 conditional GET(If-None-Match)去 revalidate;没变化时 Firebase 回一个很小的 304 Not Modified,而不是整页。这点小往返就是换”即时更新”的代价。
  • firebase hosting:rollback 命令吗?: 没有(2026 年 6 月)。从控制台回滚(Hosting → 发布历史 → Roll back),或克隆指定版本:firebase hosting:clone PROJECT:@VERSION_ID PROJECT:live

相关阅读

标签: #独立开发 #Firebase #部署 / 托管 #排查