Service Worker首屏秒开需预缓存与导航预加载双轨并行:install阶段用addAll预存静态骨架文件,同时启用navigationPreload,fetch中优先使用preloadResponse。

Service Worker 本身不预取资源,它只在请求发生后才介入;真正能“提前加载”的是导航预加载(Navigation Preload)和安装阶段的预缓存(Precache),二者必须配合使用才能实现首屏秒开。
为什么直接 fetch() 预取首页资源在 install 阶段会失败
很多人在 install 事件里写 fetch('/index.html').then(r => r.text()),结果发现控制台报错或缓存为空。这不是代码写错了,而是因为:
-
fetch()在 install 阶段发起时,浏览器尚未赋予 Service Worker 网络权限(尤其在部分 Chrome 版本中) - 响应体是
ReadableStream,未显式clone()就直接cache.put()会导致流被消费后无法再读,缓存实际为空 - 首页 HTML 往往依赖动态内容(如时间戳、用户信息),硬缓存可能造成语义错误或 CSP 拒绝
正确做法:用 precache + navigation preload 双轨并行
预缓存解决“已有资源快速返回”,导航预加载解决“新请求不卡 SW 启动”。两者不是二选一,而是必须共存:
- 在
install阶段用caches.open().addAll([...])预存静态骨架文件(/index.html、/app.css、/vendor.js),注意 HTML 文件需是构建时生成的纯静态版本(不含服务端插入) - 注册时立刻启用导航预加载:
navigator.serviceWorker.register('/sw.js').then(r => r.navigationPreload.enable()),否则默认关闭 - 在
fetch事件中优先用event.preloadResponse,它比caches.match()更快——因为它是浏览器在 SW 启动同时发出去的原始网络请求结果,没经过 JS 解析
示例关键逻辑:
self.addEventListener('fetch', event => {
if (event.request.destination === 'document') {
event.respondWith(
event.preloadResponse
.then(r => r || caches.match(event.request))
.catch(() => caches.match(event.request))
.then(r => r || fetch(event.request))
);
}
});
常见踩坑:revision 版本号没更新,缓存永远不刷新
Uniapp 或 Workbox 类工具依赖 revision 字段判断资源是否变更。但很多人只改了文件内容,忘了改 manifest.json 或 precache 清单里的 revision 值,导致:
- 新版本 JS/CSS 实际已部署,但 SW 仍返回旧缓存
- 用户强制刷新也无效,因为
caches.match()优先命中旧缓存 - 调试时看到
CacheStorage里有多个版本 cache,但 activate 阶段没清理旧版,磁盘越积越多
解决方案很简单:每次构建时自动生成带哈希的 revision,比如 "revision": "a1b2c3d4",而非手写 "1.0.0";并在 activate 事件中调用 caches.delete() 清理非当前版本的 cache。
首屏极速的关键不在“多缓什么”,而在“少等什么”
真正拖慢首屏的往往不是资源体积,而是串行等待:等 SW install → 等 SW activate → 等 fetch 触发 → 等 cache.match → 最后才 fetch 网络。Navigation Preload 把“发请求”这一步提到 SW 启动之前,把原本 300ms 的链路压到 80ms 内。但这个优化只有在用户第二次访问时才生效——第一次访问仍需靠预缓存兜底。所以不要只盯着 preloadResponse,得确保 install 阶段缓存的那几份文件,真的覆盖了 HTML 文档解析所需的最小依赖集(比如不能漏掉 font 或关键 css,否则触发 FOIT/FOUT)。










