Service Worker durch Vorabladen der Navigation beschleunigen

Mit dem Vorabladen der Navigation können Sie die Service-Worker-Startzeit überwinden, indem Sie Anfragen parallel ausführen.

Archibald
Jake Archibald

Unterstützte Browser

  • 59
  • 18
  • 99
  • 15,4

Quelle

Zusammenfassung

Das Problem

Wenn Sie eine Website aufrufen, die Abrufereignisse mit einem Service Worker verarbeitet, fordert der Browser beim Service Worker eine Antwort an. Dazu wird der Service Worker gestartet (falls er noch nicht ausgeführt wird) und das Abrufereignis wird gesendet.

Die Startzeit hängt vom Gerät und von den Bedingungen ab. Sie beträgt normalerweise etwa 50 ms. Auf Mobilgeräten sind das etwa 250 ms. In extremen Fällen (langsame Geräte, CPU-Notfälle) kann die Frequenz über 500 ms liegen. Da der Service Worker zwischen den Ereignissen für einen bestimmten Zeitraum aktiv bleibt, tritt diese Verzögerung jedoch nur gelegentlich auf, z. B. wenn der Nutzer von einem neuen Tab oder einer anderen Website zu Ihrer Website navigiert.

Die Startzeit ist kein Problem, wenn Sie aus dem Cache antworten, da der Vorteil beim Überspringen des Netzwerks größer ist als die Startverzögerung. Wenn Sie jedoch über das Netzwerk antworten...

Softwarestart
Navigationsanfrage

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

Wir werden die Startzeit weiter reduzieren, indem wir Code-Caching in V8 verwenden, Service Worker ohne Abrufereignis überspringen, Service Worker spekulativ starten und andere Optimierungen vornehmen. Die Startzeit ist jedoch immer größer als null.

Facebook hat uns auf die Auswirkungen dieses Problems aufmerksam gemacht und bat uns um eine Möglichkeit, parallele Navigationsanfragen durchzuführen:

Softwarestart
Navigationsanfrage



Und wir haben gesagt: „Ja, das scheint fair zu sein.“

„Navigation vorab laden“

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

Die Startverzögerung bleibt bestehen, aber die Netzwerkanfrage wird nicht blockiert, sodass der Nutzer die Inhalte früher erhält.

Hier ist ein Video, das zeigt, wie der Service Worker mithilfe einer while-Schleife absichtlich eine Startverzögerung von 500 ms erhält:

Hier finden Sie die Demo. Damit Sie die Vorteile des Vorabladens der Navigation nutzen können, benötigen Sie einen Browser, der diese Funktion 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();
    }
  }());
});

Du kannst navigationPreload.enable() jederzeit aufrufen oder die Funktion mit navigationPreload.disable() deaktivieren. Da dein fetch-Ereignis sie jedoch verwenden muss, ist es am besten, es im activate-Ereignis deines Service Workers zu aktivieren bzw. zu deaktivieren.

Vorab geladene Antwort verwenden

Jetzt führt der Browser Vorabladevorgänge für Navigationsvorgänge durch, aber Sie müssen immer noch die Antwort 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.
  • Bei der Anfrage handelt es sich um eine Navigationsanfrage, die Browser generieren, wenn sie Seiten laden, einschließlich iFrames.

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

Wenn Ihre Seite Daten aus dem Netzwerk benötigt, ist es am schnellsten, diese beim Service Worker anzufordern und eine einzige 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, mit der die Streams jeder Anfrage zusammengeführt werden. Das bedeutet, dass der im Cache gespeicherte Header angezeigt werden kann, während der Netzwerkcontent gestreamt wird.

Dies ist schneller als das Modell der App-Shell, da die Netzwerkanfrage zusammen mit der Seitenanfrage gestellt wird und die Inhalte ohne große Hacks gestreamt werden können.

Die Anfrage für includeURL wird jedoch durch die Startzeit des Service Workers verzögert. Auch das lässt sich mit der Funktion „Navigation Preload“ beheben, aber in diesem Fall wollen wir nicht die gesamte Seite vorab laden, sondern einen „Einschließen“-Code.

Zu diesem Zweck wird mit jeder Vorabladeanfrage ein Header gesendet:

Service-Worker-Navigation-Preload: true

Der Server kann diese Methode verwenden, um für Vorabladeanfragen für die Navigation andere Inhalte zu senden als für normale Navigationsanfragen. Denken Sie einfach 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')
];

Header ändern

Standardmäßig hat der Header Service-Worker-Navigation-Preload den Wert true. Sie können ihn jedoch beliebig festlegen:

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

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

Status abrufen

Sie können den Status des Vorabladens der Navigation 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 Hilfe in diesem Artikel. Vielen Dank an alle, die an der Standardisierung beteiligt sind

Teil der Videoreihe „Neu interoperable“