你部署了。*.web.app 上新版本已经在跑。朋友打开自定义域名,看到的还是昨天的标题。基本都是缓存的问题——Firebase 的 CDN、浏览器,或者两者一起。改一个 firebase.json 里的 cache header 再部署,就修好了。
问题背景
Firebase Hosting 在 CDN 边缘缓存资源,同时让浏览器也缓存。没显式 Cache-Control 的文件默认大约 1 小时。这对图片 / JS 没什么问题,但对 HTML 是灾难——访客要等一小时才能看到更新。正确做法是在 firebase.json 里按文件类型配置缓存头,外加 service worker 自律(如果你有的话)。
判断标准
- 部署成功了,新内容只有无痕窗口能看到。
- 手机看到新的、电脑看到旧的(或反过来)——浏览器缓存状态不同。
- 自定义域名是旧内容,
*.web.app是新——边缘缓存按 host 区分。 - 老的 service worker 一直返回缓存里的旧资源。
curl -I看 HTML URL 没有cache-control头(用了默认)。
快速结论
HTML 设 Cache-Control: no-cache, max-age=0,带 hash 的静态资源设 max-age=31536000, immutable,然后部署一次——下一个请求就拿到新版本。
开始前准备
- 构建输出目录要和
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();
});
- 某条路径 CDN 边缘还是陈旧时,要么改一下 URL 强制 miss(带版本路径 + redirect),要么 release 来回滚一遍:
firebase hosting:releases:list
firebase hosting:clone yourproject:live yourproject:live --version <prev>
firebase deploy --only hosting # 再前进一次,边缘重新分发
执行检查清单
- HTML、hashed 资源、图片、sitemap 在
firebase.json里都有显式Cache-Control。 - 每次部署后
curl -sI验证响应头。 - service worker(如果有)每次部署递增版本号 +
skipWaiting()。 - build 产物的
/_astro/**或等价目录文件名带 hash。
上线后验证
- 用不同网络(或 WebPageTest 这种远程工具)
curl -sI——确认边缘缓存确实更新了,不是只你本地节点。 - 无痕 + 未登录浏览器都立刻看到新版本。
- DevTools → Application → Service Workers 显示新版本已激活。
容易踩的坑
- HTML 留默认约 1 小时缓存,每次部署后追”残影”追一小时。
- HTML 上设
immutable——浏览器永远不再 revalidate,硬刷都没用。 - 框架的 cache header 和
firebase.json冲突——后写的赢,devtools 或curl看实际响应。 - service worker 有 bug,部署后永远返回旧 bundle。
- 改完配置后 CDN 边缘缓存还在——等 5 分钟或 release 回滚再前进强制重发。
- 本想用
no-cache却写成no-store——no-store把 back/forward cache 也关了。
FAQ
- 怎么让所有人立刻刷新?: 改一下 HTML 重新部署。边缘缓存按新版本失效,浏览器有长缓存的就用
no-cache触发 revalidate。 - 部署时 Firebase 会清 CDN 缓存吗?: 会,针对受影响的路径。新内容大多数地区几秒内传到边缘。
- 不同路径能设不同缓存吗?: 能。
headers数组支持多个source模式,每个独立配——同一个 header name 由首条匹配胜出。 - 页面新了 CSS 还旧——为啥?: CSS 文件名可能没带 hash。要么 build 时加 hash,要么把 CSS 缓存时间调短。
no-cache会损性能吗?: 对静态 HTML 影响小。浏览器还是用本地副本走 conditional GET(If-None-Match),网络往返很轻,频繁 revalidate 是换”即时更新”的代价。