Schnellere mehrseitige Anwendungen mit Streams

Heutzutage verwenden Websites und Web-Apps, wenn Sie es vorziehen, eher eines von zwei Navigationsschemata:

  • Die Navigationsschema-Browser stellen standardmäßig bereit, d. h. Sie geben eine URL in die Adressleiste Ihres Browsers ein und eine Navigationsanfrage gibt ein Dokument als Antwort zurück. Klicken Sie dann auf einen Link, wodurch das aktuelle Dokument in ein anderes, ad infinitum, entladen wird.
  • Das Muster einer Single-Page-Anwendung. Dabei wird eine erste Navigationsanfrage zum Laden der Anwendungs-Shell gestellt. Sie stützt sich auf JavaScript, um die Anwendungs-Shell mit vom Client gerendertes Markup mit Inhalten aus einer Back-End-API für jede "Navigation" zu füllen.

Die Befürworter haben die Vorteile der einzelnen Ansätze gewürdigt:

  • Das von Browsern standardmäßig bereitgestellte Navigationsschema ist stabil, da für Routen kein JavaScript erforderlich ist. Das Rendern von Markups durch den Client mithilfe von JavaScript kann außerdem ein kostspieliger Prozess sein. Dies bedeutet, dass bei Low-End-Geräten Inhalte verzögert angezeigt werden können, weil das Gerät die Verarbeitung von Skripts blockiert, die Inhalte bereitstellen.
  • Single-Page-Anwendungen (SPAs) hingegen ermöglichen nach dem anfänglichen Ladevorgang möglicherweise eine schnellere Navigation. Anstatt sich auf den Browser zu verlassen, um ein Dokument für ein völlig neues Dokument zu entladen (und dies bei jeder Navigation zu wiederholen), können sie ein schnelleres, App-ähnliches Format bieten. auch wenn JavaScript erforderlich ist.

In diesem Beitrag beschäftigen wir uns mit einer dritten Methode, um ein Gleichgewicht zwischen den beiden oben beschriebenen Ansätzen herzustellen: die Verwendung eines Service Workers, der die gemeinsamen Elemente einer Website, z. B. Header- und Footer-Markups, vorab im Cache speichert, und die Verwendung von Streams, um dem Client so schnell wie möglich eine HTML-Antwort zu senden. Dabei wird weiterhin das standardmäßige Navigationsschema des Browsers verwendet.

Warum sollte ich HTML-Antworten in einem Service Worker streamen?

Streaming ist etwas, das Ihr Webbrowser bereits tut, wenn er Anfragen stellt. Dies ist im Zusammenhang mit Navigationsanforderungen äußerst wichtig, da so sichergestellt wird, dass der Browser nicht blockiert wird, während er auf die gesamte Antwort wartet, bevor er mit dem Parsen von Dokument-Markup und dem Rendern einer Seite beginnen kann.

Ein Diagramm, das Nicht-Streaming-HTML im Vergleich zu Streaming-HTML darstellt Im ersten Fall wird die gesamte Markup-Nutzlast erst verarbeitet, wenn sie eintrifft. Im zweiten Beispiel wird das Markup schrittweise verarbeitet, sobald es in Blöcken aus dem Netzwerk eingeht.

Für Service Worker ist das Streaming etwas anders, da die JavaScript Streams API verwendet wird. Die wichtigste Aufgabe, die ein Service Worker erfüllt, ist das Abfangen und Beantworten von Anfragen, einschließlich Navigationsanfragen.

Diese Anfragen können auf verschiedene Weise mit dem Cache interagieren. Ein gängiges Caching-Muster für Markups ist jedoch, zuerst eine Antwort vom Netzwerk zu verwenden, aber auf den Cache zurückzugreifen, wenn eine ältere Kopie verfügbar ist. Optional kann eine allgemeine Fallback-Antwort bereitgestellt werden, falls sich keine brauchbare Antwort im Cache befindet.

Dies ist ein bewährtes Markup-Muster, das gut funktioniert, aber zwar die Zuverlässigkeit im Hinblick auf den Offline-Zugriff verbessert, aber keine inhärenten Leistungsvorteile für Navigationsanfragen bietet, die auf einer Netzwerk- oder Netzwerkstrategie basieren. Hier kommt Streaming ins Spiel. Wir sehen uns an, wie Sie mit dem Streams API-basierten workbox-streams-Modul in Ihrem Workbox-Service-Worker Navigationsanfragen auf Ihrer mehrseitigen Website beschleunigen können.

Eine typische Webseite aufschlüsseln

Strukturell betrachtet haben Websites tendenziell gemeinsame Elemente, die auf jeder Seite vorhanden sind. Eine typische Anordnung von Seitenelementen sieht häufig so aus:

  • Überschrift.
  • Inhalt.
  • Fußzeile

Am Beispiel von web.dev sieht diese Aufschlüsselung gängiger Elemente so aus:

Eine Übersicht über die gängigen Elemente der web.dev-Website. Die abgegrenzten Gemeinschaftsbereiche sind als „Kopfzeile“, „Inhalt“ und „Fußzeile“ gekennzeichnet.

Das Ziel hinter der Identifizierung von Teilen einer Seite besteht darin, dass wir bestimmen, welche Teile einer Seite vorab im Cache gespeichert und abgerufen werden können, ohne das Netzwerk aufzurufen – nämlich das gemeinsame Markup für Kopf- und Fußzeilen aller Seiten – und den Teil der Seite, für den wir immer zuerst das Netzwerk aufrufen – in diesem Fall den Inhalt.

Wenn wir wissen, wie die Teile einer Seite segmentiert werden sollen, können wir einen Service Worker schreiben, der das Markup für Kopf- und Fußzeilen immer sofort aus dem Cache abruft und nur den Inhalt aus dem Netzwerk anfordert.

Wenn wir dann die Streams API über workbox-streams verwenden, können wir diese Teile zusammenfügen und sofort auf Navigationsanfragen reagieren – und dabei das erforderliche Minimum an Markup vom Netzwerk anfordern.

Streaming-Dienst-Worker erstellen

Beim Streamen von Teilinhalten in einem Service Worker gibt es viele Faktoren. Jeder Schritt des Prozesses wird dabei im Detail behandelt, angefangen bei der Strukturierung Ihrer Website.

Website in Segmente unterteilen

Bevor Sie mit dem Schreiben eines Streaming-Service-Workers beginnen können, müssen Sie drei Dinge tun:

  1. Erstellen Sie eine Datei, die nur das Header-Markup Ihrer Website enthält.
  2. Erstellen Sie eine Datei, die nur das Fußzeilen-Markup Ihrer Website enthält.
  3. Ziehen Sie den Hauptinhalt jeder Seite in eine separate Datei oder richten Sie Ihr Back-End so ein, dass nur der Seiteninhalt basierend auf einem HTTP-Anfrageheader bedingt bereitgestellt wird.

Wie Sie sich vielleicht vorstellen können, ist der letzte Schritt der schwierigste, insbesondere wenn Ihre Website statisch ist. In diesem Fall müssen Sie zwei Versionen jeder Seite erstellen: Eine Version enthält das vollständige Seiten-Markup und die andere nur den Inhalt.

Streaming Service Worker erstellen

Wenn Sie das Modul workbox-streams nicht installiert haben, müssen Sie dies zusätzlich zu den derzeit installierten Workbox-Modulen tun. In diesem konkreten Beispiel sind die folgenden Pakete erforderlich:

npm i workbox-navigation-preload workbox-strategies workbox-routing workbox-precaching workbox-streams --save

Im nächsten Schritt erstellen Sie den neuen Service Worker und speichern die Teilangaben für die Kopf- und Fußzeile vorab im Cache.

Teile vorab im Cache speichern

Zuerst erstellen Sie im Stammverzeichnis Ihres Projekts einen Service Worker mit dem Namen sw.js (oder einem anderen Dateinamen). Darin beginnen Sie mit Folgendem:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// Enable navigation preload for supporting browsers
navigationPreload.enable();

// Precache partials and some static assets
// using the InjectManifest method.
precacheAndRoute([
  // The header partial:
  {
    url: '/partial-header.php',
    revision: __PARTIAL_HEADER_HASH__
  },
  // The footer partial:
  {
    url: '/partial-footer.php',
    revision: __PARTIAL_FOOTER_HASH__
  },
  // The offline fallback:
  {
    url: '/offline.php',
    revision: __OFFLINE_FALLBACK_HASH__
  },
  ...self.__WB_MANIFEST
]);

// To be continued...

Dieser Code bewirkt Folgendes:

  1. Aktiviert das Vorabladen der Navigation für Browser, die das Vorabladen unterstützen.
  2. Speichert die Auszeichnung von Kopf- und Fußzeilen vorab im Cache. Das bedeutet, dass die Auszeichnung für Kopf- und Fußzeilen für jede Seite sofort abgerufen wird, da sie nicht vom Netzwerk blockiert wird.
  3. Statische Assets im Platzhalter __WB_MANIFEST, die die Methode injectManifest verwenden, werden vorab im Cache gespeichert.

Streamingantworten

Der größte Teil dieses Aufwands besteht darin, Ihren Service Worker dazu zu bringen, verkettete Antworten zu streamen. Trotzdem ist dies mit Workbox und workbox-streams wesentlich kürzer, als wenn Sie all dies alleine machen müssten:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// ...
// Prior navigation preload and precaching code omitted...
// ...

// The strategy for retrieving content partials from the network:
const contentStrategy = new NetworkFirst({
  cacheName: 'content',
  plugins: [
    {
      // NOTE: This callback will never be run if navigation
      // preload is not supported, because the navigation
      // request is dispatched while the service worker is
      // booting up. This callback will only run if navigation
      // preload is _not_ supported.
      requestWillFetch: ({request}) => {
        const headers = new Headers();

        // If the browser doesn't support navigation preload, we need to
        // send a custom `X-Content-Mode` header for the back end to use
        // instead of the `Service-Worker-Navigation-Preload` header.
        headers.append('X-Content-Mode', 'partial');

        // Send the request with the new headers.
        // Note: if you're using a static site generator to generate
        // both full pages and content partials rather than a back end
        // (as this example assumes), you'll need to point to a new URL.
        return new Request(request.url, {
          method: 'GET',
          headers
        });
      },
      // What to do if the request fails.
      handlerDidError: async ({request}) => {
        return await matchPrecache('/offline.php');
      }
    }
  ]
});

// Concatenates precached partials with the content partial
// obtained from the network (or its fallback response).
const navigationHandler = composeStrategies([
  // Get the precached header markup.
  () => matchPrecache('/partial-header.php'),
  // Get the content partial from the network.
  ({event}) => contentStrategy.handle(event),
  // Get the precached footer markup.
  () => matchPrecache('/partial-footer.php')
]);

// Register the streaming route for all navigation requests.
registerRoute(({request}) => request.mode === 'navigate', navigationHandler);

// Your service worker can end here, or you can add more
// logic to suit your needs, such as runtime caching, etc.

Dieser Code besteht aus drei Hauptteilen, die die folgenden Anforderungen erfüllen:

  1. Anfragen für Teilinhalte von Inhalten werden mit der Strategie NetworkFirst verarbeitet. Bei dieser Strategie wird der benutzerdefinierte Cache-Name content angegeben, der die Inhaltsteile enthält, sowie ein benutzerdefiniertes Plug-in, das verarbeitet, ob ein X-Content-Mode-Anfrageheader für Browser festgelegt wird, die kein Navigationsvorabladen unterstützen (und daher keinen Service-Worker-Navigation-Preload-Header senden). Dieses Plug-in erkennt auch, ob die letzte im Cache gespeicherte Version eines Inhaltsteils oder eine Offline-Fallback-Seite gesendet werden soll, falls keine im Cache gespeicherte Version für die aktuelle Anfrage gespeichert wird.
  2. Die Methode strategy in workbox-streams (hier Alias als composeStrategies) wird verwendet, um die vorab im Cache gespeicherten Header- und Footer-Teile mit dem vom Netzwerk angeforderten Inhaltsteil zu verketten.
  3. Das gesamte Schema ist über registerRoute für Navigationsanfragen eingerichtet.

Mit dieser Logik haben wir Streaming-Antworten eingerichtet. Möglicherweise müssen Sie jedoch einige Schritte an einem Back-End ausführen, um sicherzustellen, dass der Content aus dem Netzwerk eine Teilseite ist, die Sie mit den vorab im Cache gespeicherten Teilseiten zusammenführen können.

Wenn Ihre Website ein Back-End hat

Sie erinnern sich: Wenn das Vorabladen der Navigation aktiviert ist, sendet der Browser einen Service-Worker-Navigation-Preload-Header mit dem Wert true. Im Codebeispiel oben wurde jedoch der benutzerdefinierte Header X-Content-Mode gesendet, wenn das Vorabladen der Ereignisnavigation in einem Browser nicht unterstützt wird. Im Back-End ändern Sie die Antwort auf Basis dieser Header. In einem PHP-Back-End könnte dies für eine bestimmte Seite in etwa wie folgt aussehen:

<?php
// Check if we need to render a content partial
$navPreloadSupported = isset($_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD']) && $_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD'] === 'true';
$partialContentMode = isset($_SERVER['HTTP_X_CONTENT_MODE']) && $_SERVER['HTTP_X_CONTENT_MODE'] === 'partial';
$isPartial = $navPreloadSupported || $partialContentMode;

// Figure out whether to render the header
if ($isPartial === false) {
  // Get the header include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-header.php');

  // Render the header
  siteHeader();
}

// Get the content include
require_once('./content.php');

// Render the content
content($isPartial);

// Figure out whether to render the footer
if ($isPartial === false) {
  // Get the footer include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-footer.php');

  // Render the footer
  siteFooter();
}
?>

Im obigen Beispiel werden die Inhaltsteile als Funktionen aufgerufen, die den Wert von $isPartial verwenden, um zu ändern, wie die Teile gerendert werden. So kann die Renderer-Funktion content beispielsweise ein bestimmtes Markup in Bedingungen nur einschließen, wenn sie als Teil abgerufen werden. Dies wird in Kürze behandelt.

Hinweise

Bevor Sie einen Service Worker bereitstellen, um Teile davon zu streamen und zusammenzufügen, müssen Sie einige Dinge beachten. Es stimmt, dass die Verwendung eines Service Workers auf diese Weise das Standardnavigationsverhalten des Browsers nicht grundlegend ändert. Es gibt jedoch einige Dinge, die Sie möglicherweise beheben müssen.

Seitenelemente während der Navigation aktualisieren

Der schwierigste Teil dieses Ansatzes besteht darin, dass einige Dinge auf dem Client aktualisiert werden müssen. Das Pre-Caching des Header-Markups bedeutet beispielsweise, dass die Seite denselben Inhalt im <title>-Element hat und sogar die Verwaltung des Ein/Aus-Status für Navigationselemente bei jeder Navigation aktualisiert werden muss. Diese und andere Dinge müssen möglicherweise für jede Navigationsanfrage auf dem Client aktualisiert werden.

Du kannst das Problem umgehen, indem du ein Inline-<script>-Element in den Teil des Inhalts einfügst, der aus dem Netzwerk stammt, um einige wichtige Dinge zu aktualisieren:

<!-- The JSON below contains information about the current page. -->
<script id="page-data" type="application/json">'{"title":"Sand Wasp &mdash; World of Wasps","description":"Read all about the sand wasp in this tidy little post."}'</script>
<script>
  const pageData = JSON.parse(document.getElementById('page-data').textContent);

  // Update the page title
  document.title = pageData.title;
</script>
<article>
  <!-- Page content omitted... -->
</article>

Dies ist nur ein Beispiel dafür, was Sie tun können, wenn Sie sich für diese Service Worker-Einrichtung entscheiden. Bei komplexeren Anwendungen mit Nutzerinformationen müssen Sie beispielsweise einige relevante Daten in einem Web Store wie localStorage speichern und die Seite von dort aus aktualisieren.

Umgang mit langsamen Netzwerken

Ein Nachteil beim Streaming von Antworten mit Markup aus dem Precache kann auftreten, wenn die Netzwerkverbindungen langsam sind. Das Problem besteht darin, dass die Header-Auszeichnung aus dem Precache sofort eintrifft, aber die Inhalte teilweise aus dem Netzwerk nach dem anfänglichen Paint des Header-Auszeichnungs-Markups recht lange dauern können.

Dies kann verwirrend sein. Wenn die Netzwerke sehr langsam sind, kann es sogar das Gefühl haben, dass die Seite beschädigt ist und nicht weiter gerendert wird. In solchen Fällen können Sie ein Ladesymbol oder eine Nachricht im Markup des Inhaltsteils einfügen, die Sie ausblenden können, sobald der Inhalt geladen wurde.

Eine Möglichkeit dafür ist CSS. Angenommen, der Teil des Headers endet mit einem öffnenden <article>-Element, das so lange leer ist, bis der Inhaltsteil eingeht, um ihn zu füllen. Sie können eine CSS-Regel in etwa so schreiben:

article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Das funktioniert, allerdings wird auf dem Client unabhängig von der Netzwerkgeschwindigkeit eine Lademeldung angezeigt. Wenn Sie ein seltsames Aufleuchten von Nachrichten vermeiden möchten, können Sie diesen Ansatz ausprobieren. Dabei verschachteln wir den Selektor im obigen Snippet in einer slow-Klasse:

.slow article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Von hier aus können Sie JavaScript-Teil im Header verwenden, um den effektiven Verbindungstyp (zumindest in Chromium-Browsern) zu lesen und die slow-Klasse bei ausgewählten Verbindungstypen zum Element <html> hinzuzufügen:

<script>
  const effectiveType = navigator?.connection?.effectiveType;

  if (effectiveType !== '4g') {
    document.documentElement.classList.add('slow');
  }
</script>

Dadurch wird sichergestellt, dass effektive Verbindungstypen, die langsamer sind als der Typ 4g, eine Lademeldung erhalten. Anschließend können Sie im Inhaltsteil ein Inline-<script>-Element einfügen, um die Klasse slow aus dem HTML-Code zu entfernen und die Ladenachricht zu entfernen:

<script>
  document.documentElement.classList.remove('slow');
</script>

Fallback-Antwort bereitstellen

Angenommen, Sie setzen für Ihre Inhaltsteile eine netzwerkorientierte Strategie ein. Wenn der Nutzer offline ist und eine Seite aufruft, die er bereits besucht hat, ist er abgedeckt. Wenn sie jedoch eine Seite aufrufen, die sie noch nicht besucht haben, erhalten sie nichts. Um dies zu vermeiden, müssen Sie eine Fallback-Antwort bereitstellen.

Der Code, der zum Erstellen einer Fallback-Antwort erforderlich ist, ist in früheren Codebeispielen demonstriert. Der Prozess umfasst zwei Schritte:

  1. Offline-Fallback-Antwort im Voraus im Cache speichern
  2. Richte im Plug-in einen handlerDidError-Callback für deine netzwerkorientierte Strategie ein, um den Cache auf die zuletzt aufgerufene Version einer Seite zu prüfen. Wenn nie auf die Seite zugegriffen wurde, müssen Sie die matchPrecache-Methode aus dem workbox-precaching-Modul verwenden, um die Fallback-Antwort aus dem Precache abzurufen.

Caching und CDNs

Wenn Sie in Ihrem Service Worker dieses Streamingmuster verwenden, prüfen Sie, ob Folgendes auf Ihre Situation zutrifft:

  • Sie verwenden ein CDN oder einen anderen Zwischen- oder öffentlichen Cache.
  • Du hast einen Cache-Control-Header mit einer max-age- und/oder s-maxage-Anweisung(en) ungleich null in Kombination mit der public-Anweisung angegeben.

Wenn beides auf Sie zutrifft, kann der Zwischen-Cache Antworten auf Navigationsanfragen speichern. Beachten Sie jedoch, dass bei Verwendung dieses Musters möglicherweise zwei verschiedene Antworten für eine gegebene URL ausgegeben werden:

  • Die vollständige Antwort, die die Kopfzeile, den Inhalt und die Fußzeile enthält.
  • Die Teilantwort, die nur den Inhalt enthält.

Dies kann zu unerwünschtem Verhalten führen und die Auszeichnung von Kopf- und Fußzeilen verdoppeln, da der Service Worker möglicherweise eine vollständige Antwort aus dem CDN-Cache abruft und diese mit Ihrem vorab im Cache gespeicherten Header- und Footer-Markup kombiniert.

Um dies zu umgehen, müssen Sie sich auf den Vary-Header verlassen, der sich auf das Caching-Verhalten auswirkt, indem im Cache speicherbare Antworten auf einen oder mehrere Header in der Anfrage codiert werden. Da wir die Antworten auf Navigationsanfragen basierend auf den Service-Worker-Navigation-Preload- und benutzerdefinierten X-Content-Mode-Anfrageheadern variieren, müssen wir diesen Vary-Header in der Antwort angeben:

Vary: Service-Worker-Navigation-Preload,X-Content-Mode

Mit dieser Kopfzeile unterscheidet der Browser zwischen vollständigen und teilweisen Antworten auf Navigationsanfragen und vermeidet so Probleme mit doppeltem Header- und Footer-Markup sowie jeglichen Zwischen-Caches.

Das Ergebnis

Bei den meisten Tipps zur Ladezeit geht es darum, den Nutzern zu zeigen, was Sie erhalten haben. Warten Sie nicht, bis alle Informationen vorliegen, bevor Sie dem Nutzer etwas zeigen.

<ph type="x-smartling-placeholder"></ph> Jake Archibald in Fun Hacks for Improving Content

Browser sind hervorragend, wenn es um die Verarbeitung von Antworten auf Navigationsanfragen geht, selbst bei umfangreichen HTML-Antworttexten. Standardmäßig streamen und verarbeiten Browser schrittweise das Markup in Blöcken, die lange Aufgaben vermeiden, was sich positiv auf die Startleistung auswirkt.

Das funktioniert zu unserem Vorteil, wenn wir ein Streamingdienst-Worker-Muster verwenden. Wenn Sie von Anfang an auf eine Anfrage aus dem Service Worker-Cache antworten, erfolgt der Start der Antwort nahezu sofort. Wenn Sie vorab im Cache gespeichertes Markup für Kopf- und Fußzeile mit einer Antwort aus dem Netzwerk kombinieren, erzielen Sie einige nennenswerte Leistungsvorteile:

  • Time to First Byte (TTFB) wird häufig stark verkürzt, da das erste Byte der Antwort auf eine Navigationsanfrage sofort gesendet wird.
  • First Contentful Paint (FCP) ist sehr schnell, da das vorab im Cache gespeicherte Header-Markup einen Verweis auf ein im Cache gespeichertes Stylesheet enthält. Das bedeutet, dass die Seite sehr schnell dargestellt wird.
  • In einigen Fällen kann auch der Largest Contentful Paint (LCP) schneller sein, insbesondere wenn das größte Bildschirmelement über den vorab im Cache gespeicherten Header-Teil bereitgestellt wird. Dennoch kann die schnellstmögliche Bereitstellung von etwas aus dem Service Worker-Cache zusammen mit kleineren Markup-Nutzlasten zu einem besseren LCP führen.

Die Einrichtung und Iteration von Streamingarchitekturen kann etwas kompliziert sein, aber die Komplexität ist theoretisch oft nicht ärgerlich als SPAs. Der Hauptvorteil ist, dass Sie das standardmäßige Navigationsschema des Browsers nicht ersetzen, sondern es verbessern.

Noch besser ist, dass Workbox diese Architektur nicht nur möglich macht, sondern auch einfacher, als wenn Sie sie selbst implementieren würden. Probieren Sie es auf Ihrer eigenen Website aus und finden Sie heraus, wie viel schneller eine mehrseitige Website für Nutzer vor Ort sein kann.

Ressourcen