本文介绍一种高效、可维护的 Service Worker 缓存策略,涵盖预缓存过滤、关键资源筛选、运行时缓存空间管理,以及如何优雅地关闭 Service Worker。适用于大多数中大型 Web App,尤其是资源量大、更新频繁的项目。
一、背景与痛点
Service Worker 作为 PWA 的核心,能极大提升 Web App 的离线体验和加载速度。但如果缓存策略设计不当,容易出现:
- 初次加载慢:预缓存资源过多,影响首屏体验
- 缓存污染:旧资源长期残留,浪费存储空间
- 更新不及时:新版本资源未能及时生效
二、实施方案
1. 预缓存优化
预缓存指在 Service Worker 安装阶段,将指定资源提前下载并缓存。这样用户下次访问时可直接从本地获取,提升加载速度。
1.1 现状与问题
在实际开发中,很多团队会直接使用 Workbox 的 workbox.precaching
模块来实现预缓存。
Workbox 的机制:
- 构建工具(如 webpack workbox 插件)会自动生成一个包含所有静态资源的清单(
__WB_MANIFEST
)。 - 直接调用
workbox.precaching.precacheAndRoute(__WB_MANIFEST)
,会将清单中的所有资源(HTML/JS/CSS/图片/字体等)都加入预缓存。
这样做的风险:
- 如果资源量大,首次访问时 Service Worker 安装阶段会下载所有资源,导致首屏慢、带宽压力大。
- 某些资源(如图片、字体、视频)并非每次都用到,预缓存它们会造成资源浪费。
1.2 预缓存过滤策略
- 过滤掉图片、视频、字体等非关键资源
- 只对高频访问页面的 Chunk 进行预缓存
js
const precacheManifest = self.__WB_MANIFEST;
const importantJsChunks = [
'home',
'user-profile',
'dashboard',
// ... 仅列出高频页面
];
const filteredPrecache = precacheManifest.filter(item => {
// 过滤图片、字体、视频等
if (item.url.match(/.(png|jpg|jpeg|svg|gif|webp|mp4|woff2?|ttf|otf)$/i)) {
return false;
}
// 只缓存高频 JS Chunk
if (item.url.endsWith('.js')) {
return importantJsChunks.some(name => item.url.includes(name));
}
return true; // 其他如 HTML/CSS
});
workbox.precaching.precacheAndRoute(filteredPrecache);
2、运行时缓存空间优化
2.1 问题分析
- 资源(如图片、字体)通常内容稳定,适合长期缓存
- 业务 JS/CSS 频繁变更,需按版本区分缓存空间,避免缓存污染
2.2 缓存空间划分
- 图片/字体/第三方库:固定缓存空间,长期复用
- 业务 JS/CSS:按版本号动态命名缓存空间,发版自动切换
js
const appVersion = Date.now(); // 或用实际版本号
const runtimeCacheConfig = [
{
type: 'CacheFirst',
route: /.*.(png|jpg|jpeg|svg|gif|webp)$/,
name: 'app-cache:images',
max: 300,
time: 90 * 24 * 60 * 60, // 90天
},
{
type: 'CacheFirst',
route: /.*.(woff2?|ttf|otf)$/,
name: 'app-cache:fonts',
max: 20,
time: 365 * 24 * 60 * 60, // 1年
},
{
type: 'CacheFirst',
route: //cdn/.*.js$/,
name: 'app-cache:cdn-libs',
max: 50,
time: 90 * 24 * 60 * 60,
},
{
type: 'StaleWhileRevalidate',
route: /^(?!.*/cdn/).*.(js|css)$/,
name: `app-cache:static-v${appVersion}`,
max: 30,
},
];
runtimeCacheConfig.forEach(cfg => {
const strategy = {
'CacheFirst': new workbox.strategies.CacheFirst({ cacheName: cfg.name }),
'StaleWhileRevalidate': new workbox.strategies.StaleWhileRevalidate({ cacheName: cfg.name }),
// ...
}[cfg.type];
workbox.routing.registerRoute(({url}) => cfg.route.test(url.pathname), strategy);
});
3. Service Worker 快速下线机制
Service Worker 一旦注册,除非用户手动清理或代码主动注销,否则会一直生效。若线上出现严重 bug,需有"紧急开关"机制。
3.1 实现思路
- 通过远程配置文件(如 JS 文件)控制 Service Worker 是否注册
- 配置文件需不可缓存,可独立上线
开关文件示例 (如 https://your-cdn.com/sw-switch.js
):
js
// sw-switch.js
window.SW_TURN_OFF = true; // true 表示关闭 SW,false 表示允许注册
页面引入与判断:
html
<script src="https://your-cdn.com/sw-switch.js"></script>
<script>
if (window.SW_TURN_OFF === true) {
// 注销已注册的 Service Worker
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.unregister());
});
} else {
// 正常注册 Service Worker
navigator.serviceWorker.register('/sw.js');
}
</script>
如何保证配置文件不可缓存?
为了让 Service Worker 的开关配置能实时生效,可以考虑以下措施:
-
服务端设置 HTTP 头 :为 sw-switch.js 设置
Cache-Control: no-store
或Cache-Control: no-cache, must-revalidate
,确保浏览器每次都去服务器拉取最新文件。 -
前端加时间戳:在页面引入 sw-switch.js 时拼接时间戳参数,例如:
html<script src="https://your-cdn.com/sw-switch.js?t=1688888888888"></script>
或用 JS 动态插入,确保每次请求的 URL 不同,绕过缓存。
-
CDN 配置:如文件托管在 CDN,建议将该文件的缓存策略设置为"不缓存"或"每次回源"。
团队的CDN平台支持直接配置文件的缓存策略,因此采用了第三种方法。
五、总结与最佳实践
- 预缓存只选关键资源,减少首屏压力
- 运行时缓存空间分离,长期资源与业务资源分开管理
- 支持远程开关,可随时下线 Service Worker