Service Worker durch Vorabladen der Navigation beschleunigen

Mit der Navigationsvorab-Ladefunktion können Sie die Startzeit von Service Workern verkürzen, indem Sie Anfragen parallel stellen.

Jake Archibald
Jake Archibald

Unterstützte Browser

  • Chrome: 59.
  • Edge: 18.
  • Firefox: 99.
  • Safari: 15.4.

Quelle

Zusammenfassung

Das Problem

Wenn Sie eine Website aufrufen, auf der ein Service Worker zum Verarbeiten von Abrufereignissen verwendet wird, fordert der Browser den Service Worker um eine Antwort. Dazu wird der Service Worker gestartet (falls er noch nicht ausgeführt wird) und das Abrufereignis gesendet.

Die Bootzeit hängt vom Gerät und den Bedingungen ab. Sie beträgt in der Regel etwa 50 ms. Auf Mobilgeräten sind es eher 250 Millisekunden. In extremen Fällen (langsame Geräte, überlastete CPU) kann es über 500 ms dauern. Da der Dienst-Worker jedoch zwischen Ereignissen eine vom Browser festgelegte Zeit aktiv bleibt, tritt diese Verzögerung nur gelegentlich auf, z. B. wenn der Nutzer von einem neuen Tab oder einer anderen Website zu Ihrer Website wechselt.

Die Bootzeit ist kein Problem, wenn Sie aus dem Cache antworten, da der Vorteil, das Netzwerk zu überspringen, größer ist als die Bootverzögerung. Wenn Sie jedoch über das Netzwerk antworten…

SW-Boot
Navigationsanfrage

Die Netzwerkanfrage wird durch das Starten des Dienstarbeiters verzögert.

Wir arbeiten weiter daran, die Bootzeit zu verkürzen, indem wir Code-Caching in V8, Dienstworker ohne Fetch-Ereignis überspringen, Dienstworker spekulativ starten und andere Optimierungen vornehmen. Die Bootzeit ist jedoch immer größer als null.

Facebook hat uns auf die Auswirkungen dieses Problems aufmerksam gemacht und um eine Möglichkeit gebeten, Navigationsanfragen parallel auszuführen:

SW-Boot
Navigationsanfrage

Navigationsvorabdaten helfen weiter

Mit der Navigationsvorab-Ladefunktion können Sie festlegen, dass die Netzwerkanfrage gestartet werden soll, während der Service Worker gestartet wird.

Die Startverzögerung ist weiterhin vorhanden, blockiert aber nicht die Netzwerkanfrage, sodass der Nutzer die Inhalte früher erhält.

Hier sehen Sie ein Video, in dem der Dienst-Worker mit einer While-Schleife eine bewusste Startverzögerung von 500 Millisekunden erhält:

Hier ist die Demo selbst. Damit Sie die Vorteile der Navigationsvorab-Ladefunktion nutzen können, benötigen Sie einen Browser, der diese Funktion unterstützt.

Navigationsvorab-Download aktivieren

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Enable navigation preloads!
      await self.registration.navigationPreload.enable();
    }
  }());
});

Sie können navigationPreload.enable() jederzeit aufrufen oder mit navigationPreload.disable() deaktivieren. Da Ihr fetch-Ereignis jedoch davon Gebrauch machen muss, sollten Sie es im activate-Ereignis Ihres Service Workers aktivieren und deaktivieren.

Vorab geladene Antwort verwenden

Der Browser führt jetzt vorab ein Preloading für Navigationen durch, Sie müssen die Antwort aber trotzdem verwenden:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

event.preloadResponse ist ein Versprechen, das mit einer Antwort abgeschlossen wird, wenn:

  • Das Vorabladen der Navigation ist aktiviert.
  • Die Anfrage ist eine GET-Anfrage.
  • Die Anfrage ist eine Navigationsanfrage, die Browser beim Laden von Seiten generieren, einschließlich iframes.

Andernfalls ist event.preloadResponse zwar noch vorhanden, wird aber auf undefined umgeleitet.

Wenn für Ihre Seite Daten aus dem Netzwerk benötigt werden, ist es am schnellsten, sie im Service Worker anzufordern und eine einzelne gestreamte Antwort zu erstellen, die Teile aus dem Cache und Teile aus dem Netzwerk enthält.

Nehmen wir an, wir möchten einen Artikel anzeigen:

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const includeURL = new URL(url);
  includeURL.pathname += 'include';

  if (isArticleURL(url)) {
    event.respondWith(async function() {
      // We're going to build a single request from multiple parts.
      const parts = [
        // The top of the page.
        caches.match('/article-top.include'),
        // The primary content
        fetch(includeURL)
          // A fallback if the network fails.
          .catch(() => caches.match('/article-offline.include')),
        // The bottom of the page
        caches.match('/article-bottom.include')
      ];

      // Merge them all together.
      const {done, response} = await mergeResponses(parts);

      // Wait until the stream is complete.
      event.waitUntil(done);

      // Return the merged response.
      return response;
    }());
  }
});

In der Abbildung oben ist mergeResponses eine kleine Funktion, die die Streams der einzelnen Anfragen zusammenführt. So können wir den im Cache gespeicherten Header anzeigen, während die Netzwerkinhalte gestreamt werden.

Das ist schneller als das App-Shell-Modell, da die Netzwerkanfrage zusammen mit der Seitenanfrage erfolgt und der Inhalt ohne große Hacks gestreamt werden kann.

Die Anfrage für includeURL wird jedoch durch die Startzeit des Dienst-Workers verzögert. Wir können auch die Navigationsvorab-Ladefunktion verwenden, um das Problem zu beheben. In diesem Fall möchten wir jedoch nicht die gesamte Seite, sondern ein Include-Element vorab laden.

Dazu wird mit jeder Preloading-Anfrage ein Header gesendet:

Service-Worker-Navigation-Preload: true

Der Server kann anhand dieser Informationen unterschiedliche Inhalte für Navigations-Preload-Anfragen senden als für eine normale Navigationsanfrage. Denken Sie daran, einen Vary: Service-Worker-Navigation-Preload-Header hinzuzufügen, damit Caches wissen, dass sich Ihre Antworten unterscheiden.

Jetzt können wir die Preloading-Anfrage verwenden:

// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
  // Else do a normal fetch
  .then(r => r || fetch(includeURL))
  // A fallback if the network fails.
  .catch(() => caches.match('/article-offline.include'));

const parts = [
  caches.match('/article-top.include'),
  networkContent,
  caches.match('/article-bottom')
];

Kopfzeile ändern

Standardmäßig ist der Wert der Service-Worker-Navigation-Preload-Überschrift true. Sie können ihn jedoch frei festlegen:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
  console.log('Done!');
});

Sie können sie beispielsweise auf die ID des letzten Beitrags festlegen, den Sie lokal im Cache gespeichert haben, damit der Server nur neuere Daten zurückgibt.

Status abrufen

Sie können den Status der Navigationsvorabdaten mit getState prüfen:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.getState();
}).then(state => {
  console.log(state.enabled); // boolean
  console.log(state.headerValue); // string
});

Vielen Dank an Matt Falkenhagen und Tsuyoshi Horo für ihre Arbeit an dieser Funktion und ihre Unterstützung bei diesem Artikel. Und ein großes Dankeschön an alle, die an der Standardisierungsarbeit beteiligt waren.