Streame deinen Weg zu Sofortantworten

Jan Posnick
Jeff Posnick

Jeder, der Service Worker verwendet hat, kann Ihnen sagen, dass sie bis ganz abwärts asynchron sind. Sie stützen sich ausschließlich auf ereignisbasierte Schnittstellen wie FetchEvent und verwenden Promise, um zu signalisieren, dass asynchrone Vorgänge abgeschlossen sind.

Asynchronität ist ebenso wichtig, wenn auch weniger für Entwickler sichtbar, wenn es um Antworten geht, die vom Abruf-Event-Handler eines Service Workers zurückgegeben werden. Streamingantworten sind hier der Goldstandard: Sie ermöglichen es der Seite, die die ursprüngliche Anfrage gestellt hat, mit der Antwort zu arbeiten, sobald der erste Datenblock verfügbar ist, und möglicherweise Parser, die für das Streaming optimiert sind, um den Inhalt schrittweise anzuzeigen.

Wenn Sie Ihren eigenen fetch-Event-Handler schreiben, ist es üblich, der Methode respondWith() einfach ein Response (oder ein Promise für ein Response), das Sie über fetch() oder caches.match() erhalten, zu übergeben und es einen Tag aufzurufen. Die gute Nachricht ist, dass die mit beiden Methoden erstellten Responses bereits gestreamt werden können. Die schlechte Nachricht ist, dass „manuell“ erstellte Responses bislang nicht gestreamt werden können. Hier kommt die Streams API ins Spiel.

Streams?

Ein Stream ist eine Datenquelle, die inkrementell erstellt und bearbeitet werden kann. Er bietet eine Schnittstelle zum Lesen oder Schreiben asynchroner Datenblöcke, von denen immer nur ein Teil im Arbeitsspeicher verfügbar sein kann. Vorerst interessieren wir uns für ReadableStreams, mit denen ein Response-Objekt erstellt werden kann, das an fetchEvent.respondWith() übergeben wird:

self.addEventListener('fetch', event => {
    var stream = new ReadableStream({
    start(controller) {
        if (/* there's more data */) {
        controller.enqueue(/* your data here */);
        } else {
        controller.close();
        }
    });
    });

    var response = new Response(stream, {
    headers: {'content-type': /* your content-type here */}
    });

    event.respondWith(response);
});

Die Seite, deren Anfrage das fetch-Ereignis ausgelöst hat, erhält eine Streaming-Antwort, sobald event.respondWith() aufgerufen wird. Sie liest weiter aus diesem Stream, solange der Service Worker weitere enqueue()-Daten weitergibt. Die Antwort, die vom Service Worker an die Seite gesendet wird, ist wirklich asynchron und wir haben die vollständige Kontrolle über das Füllen des Streams.

Verwendung in der Praxis

Wahrscheinlich ist Ihnen aufgefallen, dass das vorherige Beispiel einige Platzhalter-/* your data here */-Kommentare und nur wenig Implementierungsdetails enthielt. Wie würde also ein Beispiel aus der Praxis aussehen?

Jake Archibald hat (nicht überraschend!) ein hervorragendes Beispiel für die Verwendung von Streams, um eine HTML-Antwort aus mehreren im Cache gespeicherten HTML-Snippets zusammen mit „Live“-Daten zu kombinieren, die über fetch() gestreamt werden – in diesem Fall Inhalte für seinen Blog.

Wie Jake erläutert hat die Verwendung einer Streaming-Antwort den Vorteil, dass der Browser den HTML-Code während des Streams parsen und rendern kann. Dies schließt auch das erste Bit ein, das schnell aus dem Cache geladen wird, und muss nicht warten, bis der gesamte Bloginhalt abgerufen wurde. Dadurch werden die Funktionen des progressiven HTML-Renderings voll ausgenutzt. Andere Ressourcen, die ebenfalls schrittweise gerendert werden können, wie einige Bild- und Videoformate, können ebenfalls von diesem Ansatz profitieren.

Streams? Oder App-Shells?

Die bestehenden Best Practices zur Verwendung von Service Workern zur Unterstützung Ihrer Webanwendungen konzentrieren sich auf ein App Shell-Modell mit dynamischen Inhalten. Bei diesem Ansatz wird die „Shell“ Ihrer Webanwendung offen im Cache gespeichert – also der minimale HTML-, JavaScript- und CSS-Code, der zum Anzeigen Ihrer Struktur und Ihres Layouts erforderlich ist. Anschließend wird über eine clientseitige Anfrage der für jede einzelne Seite erforderliche dynamische Inhalt geladen.

Streams bieten eine Alternative zum App Shell-Modell, bei der eine umfassendere HTML-Antwort an den Browser gestreamt wird, wenn ein Nutzer eine neue Seite aufruft. Die gestreamte Antwort kann zwischengespeicherte Ressourcen nutzen, sodass sie den ersten HTML-Chunk schnell bereitstellen kann, auch wenn Sie offline sind. Am Ende sehen sie jedoch mehr wie herkömmliche, vom Server gerenderte Antworttexte aus. Wenn Ihre Webanwendung beispielsweise von einem Content-Management-System unterstützt wird, das HTML durch Zusammenfügen von Teilvorlagen auf einem Server rendert, wird dieses Modell direkt in Streamingantworten umgewandelt. Dabei wird die Vorlagenlogik im Service Worker statt auf dem Server repliziert. Wie das folgende Video zeigt, kann der Geschwindigkeitsvorteil von gestreamten Antworten für diesen Anwendungsfall deutlich sein:

Ein wichtiger Vorteil des Streamings der gesamten HTML-Antwort, der erklärt, warum es die schnellste Alternative im Video ist, besteht darin, dass HTML, das während der ersten Navigationsanfrage gerendert wird, den Streaming-HTML-Parser des Browsers voll nutzen kann. HTML-Blöcke, die nach dem Laden der Seite in ein Dokument eingefügt werden, wie es im App Shell-Modell üblich ist, können diese Optimierung nicht nutzen.

Sie befinden sich also in der Planungsphase der Service Worker-Implementierung. Welches Modell sollten Sie verwenden: Streamingantworten, die progressiv gerendert werden, oder eine einfache Shell, die mit einer clientseitigen Anfrage für dynamische Inhalte gekoppelt ist? Die Antwort hängt davon ab, ob Sie eine Implementierung haben, die auf einem CMS und Teilvorlagen basiert (Vorteil: Stream), ob Sie einzelne, große HTML-Nutzlasten erwarten, die vom progressiven Rendering (Vorteil: Stream) profitieren würden, und ob Ihre Webanwendung am besten als Single-Page-Anwendung konzipiert ist (Vorteil: App Shell) und ob Sie ein Modell benötigen, das derzeit mehrere Browser unterstützt.

Die Streamingantworten auf Basis von Service Workern stehen noch in den Kinderschuhen. Wir freuen uns darauf, die verschiedenen Modelle auszuarbeiten und vor allem weitere Tools zur Automatisierung häufiger Anwendungsfälle zu entwickeln.

Detaillierte Einblicke in Streams

Wenn Sie Ihre eigenen lesbaren Streams erstellen, reicht es möglicherweise nicht aus, controller.enqueue() einfach wahllos aufzurufen. Jake erläutert kurz, wie Sie mit den Methoden start(), pull() und cancel() einen Datenstream erstellen können, der genau auf Ihren Anwendungsfall zugeschnitten ist.

Weitere Informationen finden Sie in der Streams-Spezifikation.

Kompatibilität

Unterstützung für das Erstellen eines Response-Objekts in einem Service Worker mit einer ReadableStream als Quelle wurde in Chrome 52 hinzugefügt.

Die Service Worker-Implementierung von Firefox unterstützt noch keine Antworten, die von ReadableStreams unterstützt werden. Es gibt jedoch einen relevanten Tracking-Fehler für die Streams API-Unterstützung, dem Sie folgen können.

Der Fortschritt bei der Streams API-Unterstützung ohne Präfix in Edge sowie der Service Worker-Support können auf der Plattformstatusseite von Microsoft verfolgt werden.