A service workered gyorsítótáraz – és közben késlelteti a HTML-t

A service worker indulása a hálózati kérés elé állhat. A navigation preload párhuzamosítja a kettőt – de cache-first stratégiánál felesleges forgalmat is okozhat.

Felraktál egy service workert, hogy gyorsabb és offline-tűrőbb legyen az oldal. Aztán egy hideg indításnál a HTML később kezd el letöltődni, mint nélküle. Nem a hálózat romlott el: a böngésző előbb felébreszti a service workert, átadja neki a fetch eseményt, és csak a handleredből indított fetch() után fordul a szerverhez. A gyorsítóréteg szépen beállt a gyorsítandó dolog elé. Klasszikus.

A Service Worker specifikáció szerint a böngésző leállíthatja a workert, ha nincs feldolgozandó eseménye. Ez jó az erőforrásoknak, de egy későbbi navigációnál újra el kell indítani a scriptet. Ha a HTML-t többnyire hálózatról szolgálod ki, az indulás és a hálózati kérés egymás után következik. A navigation preload ezt a két munkát indítja el párhuzamosan.

Folyamatábra: service worker nélkül egymás után indul a worker és a HTML-kérés, navigation preloaddal pedig a két művelet párhuzamosan fut

A soros várakozás, amit magunknak építettünk

Egy network-first handler első ránézésre ártatlan:

self.addEventListener("fetch", (event) => {
  if (event.request.mode !== "navigate") return;

  event.respondWith(
    fetch(event.request).catch(() => caches.match("/offline.html")),
  );
});

Csakhogy a fetch() csak akkor fut le, amikor a worker már él és a listener sorra került. Lassú CPU-n a Chrome DevTools Network paneljén külön ServiceWorker Preparation szakaszként is látszik ez az idő. A DevTools dokumentációja nem véletlenül választja külön a worker előkészítését a hálózati várakozástól.

A böngésző előreszalad a hálózatra

Az előtöltést az aktiváláskor kell bekapcsolni. Az event.waitUntil() itt nem dísz: az aktiválási eseményt életben tartja, amíg a beállítás elkészül.

self.addEventListener("activate", (event) => {
  if (!self.registration.navigationPreload) return;
  event.waitUntil(self.registration.navigationPreload.enable());
});

Ezután navigációnál a böngésző a worker ébresztésével együtt elindítja az eredeti kérést. Az eredményt nem neked kell a Cache API-ból előásni, és nem egy második fetch()-et kell írni hozzá: a FetchEvent.preloadResponse promise adja vissza.

self.addEventListener("fetch", (event) => {
  if (event.request.mode !== "navigate") return;
  event.respondWith(handleNavigation(event));
});

async function handleNavigation(event) {
  const cached = await caches.match(event.request);

  if (cached) {
    event.waitUntil(event.preloadResponse.catch(() => undefined));
    return cached;
  }

  const preloaded = await event.preloadResponse;
  return preloaded ?? fetch(event.request);
}

A sorrend fontos. Cache-miss után előbb a már futó preloadot várjuk meg; ha rögtön fetch(event.request)-et hívunk, könnyen ugyanazért a HTML-ért küldünk még egy kérést. A normál esetben a preload létezik, a fallback a nem támogatott vagy kikapcsolt állapotot kezeli.

A kellemetlen csavar: a cache-találat nem állítja vissza az időt

Na, ezt nem tudtam. Mire a worker megtalálja a HTML-t a Cache API-ban, a navigation preload hálózati kérése már elindult. A cached választ visszaadhatod, de a bájtok egy része vagy egésze ettől még átjöhetett. Az MDN friss példája cache-találatnál is waitUntil()-lal köti az eseményhez a preload promise-t; nélküle Chromiumban a kérés megszakadhat, és konzolhiba jelenhet meg. Erre nem érdemes „optimalizálásként” építeni.

Ez a feature ezért nem univerzális turbógomb. Network-only és network-first HTML-nél természetes választás: a válasz amúgy is a szerverről jönne. Cache-first navigációnál viszont minden találat mellé spekulatív hálózati forgalmat vásárolsz. Ha az alkalmazásod tényleg stabil, előre cache-elt app shellt ad, a preload akár nettó pazarlás is lehet; ilyenkor kapcsold ki navigationPreload.disable()-lel.

Van egy fejléc is, tehát van egy cache-kulcs is

A preload kérés alapból Service-Worker-Navigation-Preload: true fejléccel érkezik; az érték a setHeaderValue() metódussal módosítható. A szerver így küldhet például csak egy streamelhető HTML-részletet, amelyet a worker helyi shell-lel rak össze. Ez már haladó mutatvány, és könnyű félrekonfigurálni: ha a válasz a fejléc miatt eltér, a web.dev útmutatója szerint a Vary: Service-Worker-Navigation-Preload fejlécnek is ott kell lennie. Különben egy köztes cache a preloadhoz szabott választ sima navigációnál is felszolgálhatja. A cache természetesen nem sejti, hogy mi volt a költő szándéka.

Mérd meg, aztán kapcsold be

A DevTools Timing nézetében keresd a worker előkészítését, szerveroldalon pedig naplózd a preload fejlécet: így a felesleges kérések is kiderülnek. A workerStart időbélyeg megmutatja, mikor indult a service worker; ha nem volt worker az útban, az értéke nulla. Egyetlen gyors laptopos újratöltésből viszont ne írj teljesítménystratégiát. Vizsgáld külön a hideg worker, cache-hit, cache-miss és offline útvonalat.

Productionben használnám, ha a service worker kezeli a navigációt, de a HTML rendszerint hálózatról érkezik. Nem használnám automatikusan egy cache-first PWA-ban, és főleg nem hagynám bekapcsolva úgy, hogy a handler figyelmen kívül hagyja a preloadResponse-t. A navigation preload jó megoldás egy nagyon konkrét soros várakozásra. Attól még nem lesz jó a cache-stratégiád – csak gyorsabban derül ki, hogy átgondoltad-e.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük