性能 · 概念
导航预加载与 Service Worker 启动时间
发布于
导航到一个由带 fetch 处理函数的 Service Worker 控制的页面时,网络请求要等 worker 启动并运行处理函数之后才能发出,所以尚未运行的 worker 会把自己的启动时间加进响应。导航预加载让浏览器在启动 worker 的同时发出导航请求,并把响应作为 event.preloadResponse 交给处理函数,这样对于来自网络的响应,启动就不再在关键路径上。
Service Worker 空闲时会被停止、按需启动。web.dev 的测量给出的启动时间约为桌面 50 ms、中端手机接近 250 ms、慢速或被节流的设备超过 500 ms(Speed up service worker with navigation preloads,web.dev)。没有预加载时这段时间与请求串行;有预加载时浏览器立即发出请求,附加 Service-Worker-Navigation-Preload: true 头让服务器能区分两者,并在处理函数索取时以该响应兑现 event.preloadResponse。该特性按注册用 registration.navigationPreload.enable() 启用,通常在 activate 中;头的值可用 setHeaderValue() 更改,让服务器对预加载请求返回部分响应(内容片段而不是整页)。支持情况为 Chrome 59、Firefox 99、Safari 15.4(BCD api.NavigationPreloadManager),方法级参考见 Service Worker 条目。
NavigationPreloadManager 存在于 Chrome 59、Firefox 99 与 Safari 15.4,并且都与 FetchEvent.preloadResponse 一起到来(BCD api.NavigationPreloadManager、api.FetchEvent.preloadResponse),仅限安全源。没有它的引擎里 registration.navigationPreload 为 undefined,event.preloadResponse 以 undefined 兑现,于是处理函数走兜底路径。
TTFB 里的时间去了哪里
Section titled “TTFB 里的时间去了哪里”以 PerformanceNavigationTiming.responseStart 测量的 Time to First Byte 包含从 workerStart 到 fetchStart 的区间,即浏览器启动 worker 的时间。从 Cache Storage 应答的重复访问中这个区间依然存在,但响应不需要网络,所以 TTFB 是启动加一次缓存读取。由网络应答的访问中,预加载让启动与请求往返重叠,TTFB 大约减少一个启动时间。
预加载无益的情况
Section titled “预加载无益的情况”- 缓存优先的导航。 处理函数从 Cache Storage 应答时,预加载的网络响应没用上,请求浪费了带宽;解决办法是不启用预加载,或只对走网络的作用域启用。
- 已经在运行的 worker。 被最近事件保持存活的 worker 没有什么可启动,预加载不改变任何东西。
Vary与缓存。 预加载请求与普通导航只差一个头,只按 URL 缓存的 CDN 可能把预加载形态的部分响应提供给普通导航。两种响应不同时服务器要带上Vary: Service-Worker-Navigation-Preload。
处理函数返回了别的响应时,未使用的 preloadResponse 会被取消,除非处理函数用 event.waitUntil() 保持它;让它被取消是正确的默认行为。
前两个示例是 Service Worker 代码;第三个在页面里测量效果。
启用预加载并使用预加载的响应
Section titled “启用预加载并使用预加载的响应”在 activate 中启用并加特性检查;在 fetch 中优先用预加载的响应,它以 undefined 兑现时退回 fetch(),非导航请求和不支持该特性的引擎都是这种情况。
self.addEventListener('activate', (event) => { event.waitUntil((async () => { if ('navigationPreload' in self.registration) { await self.registration.navigationPreload.enable(); } })());});
self.addEventListener('fetch', (event) => { if (event.request.mode !== 'navigate') return; event.respondWith((async () => { const preloaded = await event.preloadResponse; if (preloaded) return preloaded; return fetch(event.request); // 这里没有预加载:普通网络请求 })());});对非导航请求提前返回,处理函数就不会拦截它无需触碰的子资源,也缩短了它们的路径。
向预加载请求提供内容片段
Section titled “向预加载请求提供内容片段”用 setHeaderValue('fragment') 后,服务器可以只返回外壳需要的那部分,处理函数把它拼进缓存的外壳,比整页快、比两次请求省。
// Service Worker,activate 中await self.registration.navigationPreload.setHeaderValue('fragment');
// Service Worker,导航的 fetch 中const shell = await caches.match('/shell.html');const preloaded = await event.preloadResponse;if (shell && preloaded) { const fragment = await preloaded.text(); return new Response(renderShell(await shell.text(), fragment), { headers: { 'content-type': 'text/html' } });}服务器读取 Service-Worker-Navigation-Preload: fragment,并在响应上设置 Vary: Service-Worker-Navigation-Preload,让缓存把片段和整页分开。
在页面里测量启动时间
Section titled “在页面里测量启动时间”由 Service Worker 处理的导航上 workerStart 非零;它与 fetchStart 的差就是预加载所重叠的区间。
const [nav] = performance.getEntriesByType('navigation');if (nav && nav.workerStart > 0) { console.log('worker 启动', (nav.fetchStart - nav.workerStart).toFixed(1), 'ms');} else { console.log('这次导航没有 Service Worker'); // 预加载没有可隐藏的东西}把这个数字与 responseStart - requestStart 比较,就能看出在某台设备上是启动还是服务器主导了 TTFB,这个数字决定了预加载是否值得在缓存命中时浪费的那次请求。
- Speed up service worker with navigation preloads(web.dev)
- Service Workers: NavigationPreloadManager(w3.org)
- 导航预加载(Service Worker API 参考)
- 应用外壳(app shell)模型
- PerformanceObserver 与绘制计时
规范
| 规范 | 状态 |
|---|---|
| 无。 | |