你部署了。*.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 的话先找到源文件,再改缓存。
实操步骤
- 先诊断。 看慢路径上的缓存状态:
curl -sI https://yourdomain.com/ | grep -iE 'cache-control|age|x-cache'
# 典型坏态:
# age: 1840
# x-cache: HIT
# (没有 cache-control 或写着 'public, max-age=3600')
- 改
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" }
]
}
]
}
}
-
HTML 用
no-cache, max-age=0,不是no-store。no-cache是”存一份但每次都 revalidate”,浏览器还能用本地副本,只是每次都查新鲜度。no-store强制完整重新下载,浪费。 -
带 hash 的静态资源加
immutable。 只有文件名随内容变才能用。Astro 的/_astro/**和 Vite 的 hashed 输出都符合;平常的app.js不符合。 -
部署后验证边缘头:
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
-
**浏览器卡住的话,硬刷一次(Cmd+Shift+R / Ctrl+Shift+R)**清本地缓存。之后
no-cache头就会保证一直是新的。 -
有 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();
});
- 如果上线了一个有问题的版本,回滚到已知正常的版本。 注意:没有
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.json的headers为准;用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数组支持多个sourceglob 模式,每个独立配。两个模式都匹配同一个文件时,数组里排在前面的那块对该 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。