写过 PWA 的人都踩过这个坑——上了 ServiceWorker 之后第一次访问反而更慢了,SW 启动本身那几百毫秒全算在 TTFB 里。今天 Chrome 154 用一个机制把这件事零代码彻底原生化了
写过 PWA 的人都踩过这个坑——上了 ServiceWorker 之后第一次访问反而更慢了,SW 启动本身那几百毫秒全算在 TTFB 里。今天 Chrome 154 用一个机制把这件事零代码彻底原生化了。
先说清楚 SW 为什么会帮倒忙
ServiceWorker 拦截请求有两种经典玩法:一种是提前缓存好静态资源,走 Cache Storage,这确实快;另一种是用来做在线内容的缓存代理。但无论哪种,第一次访问的时候,如果 SW 还没在跑,浏览器得先把 SW 启动起来——下载脚本、执行 install 事件、注册 fetch 事件监听——然后才能真正发起网络请求。
这个过程在 Chrome 的 Web Performance Snippets 分析工具里被叫做 SW Startup Overhead,正常情况下是 100ms 以内,但如果 SW 脚本稍微大一点,或者设备性能差一些,这个数字可以轻松超过 300ms。这 300ms 全都加在 TTFB(Time To First Byte)上,页面看起来就是一直在转圈。
为了解决这个问题,Google 早在 2017 年就推出了 NavigationPreload 方案:开发者自己在 fetch handler 里调用 event.preloadResponse,让浏览器在 SW 启动的同时并行发网络请求。但代价是代码要改,还要写一堆判断逻辑——缓存命中走缓存、preload 响应存在走 preload、都没有才走网络。维护成本不低。
ServiceWorkerAutoPreload:浏览器自动帮你干了这件事
Chrome 154 开始 stepped rollout 的 ServiceWorkerAutoPreload 解决了这个问题,而且完全不需要改代码。
它的工作原理是:当浏览器发现某个页面的 ServiceWorker 还没启动、需要先去启动 SW 再发起导航请求的时候,浏览器会自动在 SW 启动的同时提前发出这个导航请求。等到 SW 启动完毕、fetch handler 开始执行,如果 handler 里调用了 fetch(event.request),浏览器会直接把之前提前发出的请求的响应交给 fetch()——不会真的再发一遍,而是消费已经存在的那个预加载响应。
整个过程不需要任何代码改动。ServiceWorker 脚本该怎么写还怎么写,fetch handler 里该 respondWith() 就 respondWith(),浏览器内部帮你把时序优化好了。
// 代码完全不用动,浏览器自动并行化
self.addEventListener("fetch", (event) => {
event.respondWith(async function () {
const cached = await caches.match(event.request);
if (cached) return cached;
// 这行 fetch() 内部自动消费了浏览器提前发出去的请求
// 不需要写 event.preloadResponse 判断
return fetch(event.request);
}());
});
和 NavigationPreload 比,好在哪
NavigationPreload 需要开发者显式修改代码,感知并处理 event.preloadResponse。ServiceWorkerAutoPreload 的目标是把这件事变成浏览器行为,让现有的 SW 代码直接受益。
| NavigationPreload | ServiceWorkerAutoPreload | |
|---|---|---|
| 代码改动 | 需要改 fetch handler | 零改动 |
| 配置 | 需在 activate 事件里手动开启 | 浏览器自动判断 |
| 预加载请求处理 | 开发者用 event.preloadResponse 消费 |
浏览器自动路由给 fetch(event.request) |
| 兼容性 | Chrome 59+ | Chrome 154+ |
一个值得注意的细节:预加载的请求只对 event.request 生效。如果你的 SW fetch handler 里顺手调用了 fetch("/api/analytics") 发埋点数据,这个请求不会走预加载通道,而是正常发一个新的网络请求——beacon 这类场景不受影响。
如果 fetch handler 返回的内容和预加载的响应不一致(比如你做了内容替换),多发出去的这个预加载请求就浪费了,浏览器会自动把它取消以节省服务器资源。
浏览器怎么决定要不要预加载
ServiceWorkerAutoPreload 被设计成可选的浏览器优化,浏览器会通过一组启发式规则判断是否适用:
- SW 当前没有在运行:这是最核心的条件,如果 SW 已经在跑,预加载就没有意义
- fetch handler 返回的是 fallback(即直接走网络、不走缓存):如果你的 SW 每次都直接
fetch(event.request)然后返回而不做缓存,浏览器判断预加载”值得” - bootstrap 时间和 fetch handler 执行时间对首屏有影响:浏览器在性能工具里能感知到这个开销
这意味着:你的 SW 代码不变,浏览器可能在某些场景下自动开启预加载,在另外一些场景下保持原行为——这个切换是浏览器自动做的,不需要你配置。
企业级控制和退出机制
Chrome 对企业用户提供了临时策略控制:
策略名:ServiceWorkerAutoPreloadEnabled
值:0 = 禁用(等 SW 启动完再发请求,恢复到之前行为)
值:1 或未设置 = 由浏览器决定是否启用
这个策略是”临时措施”,会在 Microsoft Edge 144 版本移除——说明 Chrome 团队对这个优化有足够信心,最终会变成默认行为,不需要企业手动控制。
如果企业想主动退出(而不是等浏览器自动判断),可以通过 Static Routing API 在 SW 的 install 事件里显式声明要走 fetch handler 作为来源:
self.addEventListener("install", e => {
e.addRoutes({
condition: { urlPattern: new URLPattern({}) },
source: "fetch-event"
});
});
三步判断你要不要管这件事
第一步:先查你的 SW 代码
如果你的 SW fetch handler 已经用了 NavigationPreload(即你已经在 fetch handler 里判断 event.preloadResponse),ServiceWorkerAutoPreload 和 NavigationPreload 不会同时生效——NavigationPreload 优先,你的代码继续按预期工作,不需要改动。
第二步:没上 NavigationPreload 的话,直接等 Chrome 154 全量推送
ServiceWorkerAutoPreload 是 Chrome 154 开始 stepped rollout,预计 2026 年 9 月 9 日左右稳定版。上了 PWA、用了 SW 做缓存代理的网站,只要 SW 脚本不需要升级到特殊版本,就会自动受益。
第三步:企业用户如果发现异常,48 小时内可回退
如果你的服务器流量监控里突然多了一批来自 Chrome 的预加载请求(可以从 User-Agent 或请求特征判断),而且不是预期行为,可以通过 enterprise policy 快速禁用,或者用 Static Routing API 在 SW 代码里加退出声明。
ServiceWorkerAutoPreload 解决的不是 SW 的功能问题,而是 SW 的时序问题。上了 SW 不代表你的站点缓存做对了,SW 启动本身那一段空白期如果没处理,首屏反而更慢。这个优化把 NavigationPreload 的思路内置进了浏览器,对现有代码完全透明。上了 PWA 或者用了 Workbox 这类工具做 SW 缓存的站点,Chrome 154 升级后应该能观察到 TTFB 的自然下降——前提是你的 SW 脚本没有做版本锁定或者其他阻止浏览器优化判断的特殊逻辑。
评论区
登录后可评论。