静态站点离线缓存的失效治理与版本仲裁方案
【摘要】 背景痛点当我们给静态站点开启 Service Worker 离线缓存后,会遇到几个典型问题:资源更新后,部分用户依然长时间命中旧 SW,页面资源版本不一致(HTML 是新版,图片/JS 是旧版);直接删除 cacheName 缓存容易引发闪白、资源雪崩,大量并发重新拉取;没有版本标识,无法判断当前用户加载的是哪一版站点,线上问题难以复现;回滚发布后,旧 Service Worker 不会自...
背景痛点
当我们给静态站点开启 Service Worker 离线缓存后,会遇到几个典型问题:
- 资源更新后,部分用户依然长时间命中旧 SW,页面资源版本不一致(HTML 是新版,图片/JS 是旧版);
- 直接删除
cacheName缓存容易引发闪白、资源雪崩,大量并发重新拉取; - 没有版本标识,无法判断当前用户加载的是哪一版站点,线上问题难以复现;
- 回滚发布后,旧 Service Worker 不会自动切换,用户需要强制刷新甚至清缓存。
很多方案只做了“缓存”,没做好“缓存退役”。离线能力的难点从来不是能离线访问,而是可控地升级、可控地失效。
核心设计思路
整体思路分为四块:版本埋点、缓存命名隔离、仲裁更新策略、垃圾回收。
- 构建全局站点版本号,跟随 CI 构建自动生成(Git commit hash / build timestamp);
- CacheName 带上版本标识,不同版本缓存空间物理隔离;
- SW 启动时做版本仲裁,对比页面内埋点版本与 SW 自身版本;
- 对废弃的旧缓存空间做惰性清理,避免一次性大量删除阻塞主线程。
实现改造
1. 构建阶段注入版本号
在打包脚本里自动输出版本文件,同时注入到 HTML meta。
// build-version.js
const fs = require('fs');
const buildId = process.env.GIT_COMMIT || Date.now().toString();
const versionFile = `window.__SITE_VERSION__ = "${buildId}";`;
fs.writeFileSync('./dist/version.js', versionFile);
HTML 头部注入:
<meta name="site-version" content="__BUILD_VERSION__">
<script src="/version.js"></script>
2. Workbox 缓存名带上版本
// sw.js
import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate, CacheFirst} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
const SITE_VERSION = '__BUILD_VERSION__';
const CACHE_PREFIX = 'site-static';
const CACHE_NAME = `${CACHE_PREFIX}-${SITE_VERSION}`;
// 静态资源缓存策略
registerRoute(
({request}) => ['script', 'style', 'image', 'font'].includes(request.destination),
new CacheFirst({
cacheName: CACHE_NAME,
plugins: [
new CacheableResponsePlugin({statuses: [200]})
]
})
);
// HTML 使用 stale-while-revalidate,优先展示缓存,后台更新
registerRoute(
({request}) => request.mode === 'navigate',
new StaleWhileRevalidate({
cacheName: `${CACHE_PREFIX}-html-${SITE_VERSION}`
})
);
3. SW 安装激活时,清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 不是当前版本、且属于本项目缓存前缀的,标记为待清理
if (cacheName.startsWith(CACHE_PREFIX) && cacheName !== CACHE_NAME && cacheName !== `${CACHE_PREFIX}-html-${SITE_VERSION}`) {
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
4. 页面侧版本仲裁,优雅提示刷新
页面 JS,检测 SW 版本不一致:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'waiting') {
// 发现新版本 SW 已经就绪,可以弹出提示让用户刷新
const ok = confirm('站点已有新版本,是否立即刷新?');
if (ok) newWorker.postMessage({type: 'SKIP_WAITING'});
}
})
})
})
}
在 SW 中监听 SKIP_WAITING 消息,跳过等待直接激活:
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
策略取舍
- ✅ 优点:版本隔离清晰,回滚时只要重新发布旧版本号,就能逐步覆盖用户;惰性删除不会一次性打爆带宽;可埋点统计线上版本分布。
- ⚠️ 代价:多版本缓存会短暂占用浏览器存储空间;频繁发布会残留少量旧缓存,依赖激活阶段清理;不能做到毫秒级全站立刻更新,这是 SW 模型本身的限制。
- ❌ 不推荐:全局固定 cacheName + 直接清空缓存,极易出现 HTML 和资源版本撕裂。
可观测与灰度扩展
- 上报版本信息:页面加载时把
__SITE_VERSION__、SW 状态(active/waiting/installing)上报到埋点; - 灰度版本:可以增加路由判断,对部分用户下发新 SW,其余用户继续使用稳定版本;
- 紧急熔断:增加一个远程开关 JSON,SW 拉取开关,如果开关关闭,直接禁用缓存、全部走网络。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)