Service Worker durch Vorabladen der Navigation beschleunigen

Mit dem Vorabladen der Navigation können Sie die Startzeit des Service Workers durch parallele Anfragen reduzieren.

Jake Archibald
Jake Archibald

Unterstützte Browser

  • Chrome: 59. <ph type="x-smartling-placeholder">
  • Edge: 18. <ph type="x-smartling-placeholder">
  • Firefox: 99. <ph type="x-smartling-placeholder">
  • Safari: 15.4 <ph type="x-smartling-placeholder">

Quelle

Zusammenfassung

Das Problem

Wenn Sie eine Website aufrufen, die Abrufereignisse von einem Service Worker verarbeitet, fordert der Browser den Service Worker um eine Antwort an. Dazu muss der Service Worker (falls er noch nicht ausgeführt wird) gestartet und das Fetch-Ereignis ausgelöst werden.

Die Startzeit hängt vom Gerät und den Bedingungen ab. Sie beträgt in der Regel etwa 50 ms. Auf Mobilgeräten beträgt sie etwa 250 ms. In extremen Fällen (langsame Geräte, CPU-Störung) können die Daten über 500 ms liegen. Da der Service Worker jedoch zwischen den Ereignissen für eine vom Browser festgelegte Zeit aktiv bleibt, tritt diese Verzögerung nur gelegentlich auf, z. B. wenn der Nutzer von einem neuen Tab oder von einer anderen Website zu Ihrer Website navigiert.

Die Startzeit stellt kein Problem dar, wenn Sie aus dem Cache antworten, da der Vorteil des Überspringens des Netzwerks größer als die Startverzögerung ist. Wenn du aber über das Netzwerk antwortest...

SW-Boot
Navigationsanfrage

Die Netzwerkanfrage wird durch den Start des Service Workers verzögert.

Wir arbeiten weiterhin daran, die Startzeit durch Code-Caching in V8, das Überspringen von Service Workern ohne Abruf, spekulatives Starten von Service Workern und andere Optimierungen zu reduzieren. Die Startzeit ist jedoch immer größer als null.

Facebook hat uns auf die Auswirkungen dieses Problems aufmerksam gemacht und nach einer Möglichkeit gesucht, Navigationsanfragen parallel auszuführen:

SW-Boot
Navigationsanfrage

Navigationsvorabladevorgang zur Rettung

Das Vorabladen der Navigation ist eine Funktion, mit der Sie sagen können: „Wenn der Nutzer eine GET-Navigationsanfrage stellt, starten Sie die Netzwerkanfrage, während der Service Worker hochfährt.“

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

Hier ist ein Video davon in Aktion, bei dem dem Service Worker mithilfe einer while-Schleife eine Startverzögerung von 500 ms vorgegeben wird:

Hier ist die Demo. Damit Sie die Vorteile des Vorabladens für die Navigation nutzen können, benötigen Sie einen Browser, der dies unterstützt.

Vorabladen der Navigation 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 die Funktion mit navigationPreload.disable() deaktivieren. Da es jedoch von Ihrem fetch-Ereignis verwendet werden muss, sollten Sie es im activate-Ereignis Ihres Service Workers aktivieren und deaktivieren.

Vorab geladene Antwort verwenden

Jetzt führt der Browser Vorabladevorgänge für Navigationen durch. Sie müssen die Antwort jedoch noch 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 in folgenden Fällen mit einer Antwort aufgelöst wird:

  • Vorabladen der Navigation ist aktiviert.
  • Die Anfrage ist eine GET-Anfrage.
  • Die Anforderung ist eine Navigationsanforderung, d. h. die Browser, die beim Laden von Seiten generiert werden, einschließlich iFrames.

Andernfalls ist event.preloadResponse noch vorhanden, wird jedoch mit undefined aufgelöst.

Wenn Ihre Seite Daten aus dem Netzwerk benötigt, 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.

Angenommen, 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;
    }());
  }
});

Im obigen Beispiel ist mergeResponses eine kleine Funktion, die die Streams der einzelnen Anfragen zusammenführt. Das bedeutet, dass der im Cache gespeicherte Header angezeigt werden kann, während der Netzwerkcontent gestreamt wird.

Dies ist schneller als die „App-Shell“ da die Netzwerkanfrage zusammen mit der Seitenanfrage gesendet wird und der Content ohne wichtige Hacks gestreamt werden kann.

Die Anfrage für includeURL wird jedoch um die Startzeit des Service Workers verzögert. Mit dem Vorabladen der Navigation können wir auch dieses Problem beheben. In diesem Fall soll jedoch nicht die ganze Seite vorab geladen werden, sondern ein Einschließen.

Um dies zu unterstützen, wird mit jeder Vorabladeanfrage ein Header gesendet:

Service-Worker-Navigation-Preload: true

Der Server kann diese Methode verwenden, um andere Inhalte für Navigationsvorabladeanfragen zu 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 Preload-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 lautet der Wert des Service-Worker-Navigation-Preload-Headers true, aber Sie können ihn beliebig 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 des Navigationsvorabladens mit getState abrufen:

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 die Hilfe bei diesem Artikel. Ein großes Dankeschön an alle, die an der Standardisierung beteiligt sind.