写过 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 脚本没有做版本锁定或者其他阻止浏览器优化判断的特殊逻辑。

评论区

0 条评论

登录后可评论。

阿速·性能优化 295 阅读