Sofortiges Laden von Webanwendungen mit Anwendungs-Shell-Architektur

Addi Osmani
Addy Osmani
Matt Gaunt

Eine Anwendungs-Shell ist der minimale HTML-, CSS- und JavaScript-Code, der für eine Benutzeroberfläche benötigt wird. Die Anwendungs-Shell sollte:

  • schnell laden
  • zwischengespeichert werden
  • Content dynamisch anzeigen

Eine Anwendungs-Shell ist das Geheimnis für zuverlässig gute Leistung. Stellen Sie sich die Shell Ihrer App wie ein Code-Bundle vor, das Sie in einem App-Shop veröffentlichen, wenn Sie eine native App entwickeln. Sie ist die Last, die für den Start erforderlich ist, aber möglicherweise nicht die gesamte Handlung. Sie speichert Ihre UI lokal und ruft Inhalte dynamisch über eine API ab.

App Shell: Trennung von HTML-, JS- und CSS-Shell und HTML-Inhalten

Hintergrund

Im Artikel Progressive Web-Apps von Alex Russell wird beschrieben, wie sich eine Web-App schrittweise durch Nutzung und Nutzereinwilligung ändern kann, um ein App-ähnliches Erlebnis mit Offline-Unterstützung, Push-Benachrichtigungen und der Möglichkeit zu bieten, dem Startbildschirm hinzugefügt zu werden. Dies hängt stark von der Funktionalität und den Leistungsvorteilen des Service Worker und dessen Caching-Fähigkeiten ab. So können Sie sich auf die Geschwindigkeit konzentrieren und Ihren Web-Apps die gleichen schnellen Ladevorgänge und regelmäßigen Updates bieten, die Sie von nativen Anwendungen kennen.

Um diese Funktionen in vollem Umfang nutzen zu können, müssen wir Websites umdenken: die Anwendungs-Shell-Architektur.

Sehen wir uns an, wie Sie Ihre Anwendung mit einer erweiterten Anwendungs-Shell-Architektur mit Service Worker strukturieren. Wir schauen uns sowohl das clientseitige als auch das serverseitige Rendering an und geben dir ein End-to-End-Beispiel, das du gleich ausprobieren kannst.

Um den Punkt zu betonen, zeigt das folgende Beispiel den ersten Ladevorgang einer App, die diese Architektur verwendet. Unten auf dem Bildschirm wird der Hinweis „App ist offline verwendet“ angezeigt. Wenn später ein Update der Shell verfügbar wird, können wir den Nutzer informieren, dass er auf die neue Version aktualisieren muss.

Bild eines Service Workers, der in den Entwicklertools für die Anwendungs-Shell ausgeführt wird

Was sind wieder Service Worker?

Ein Service Worker ist ein Skript, das im Hintergrund unabhängig von Ihrer Webseite ausgeführt wird. Er reagiert auf Ereignisse, einschließlich Netzwerkanfragen von Seiten, die er bereitstellt, und sendet Benachrichtigungen von Ihrem Server. Service Worker haben eine absichtlich kurze Lebensdauer. Sie wird aktiviert, wenn ein Ereignis eingeht, und wird nur so lange ausgeführt, wie sie verarbeitet werden muss.

Außerdem haben Service Worker im Vergleich zu JavaScript in einem normalen Browserkontext eine begrenzte Anzahl von APIs. Das ist die Standardeinstellung für Worker im Web. Ein Service Worker kann zwar nicht auf das DOM zugreifen, aber beispielsweise auf die Cache API zugreifen. Außerdem kann er über die Fetch API Netzwerkanfragen stellen. Die IndexedDB API und postMessage() können auch für die Datenpersistenz und das Messaging zwischen dem Service Worker und den von ihm kontrollierten Seiten verwendet werden. Von deinem Server gesendete Push-Ereignisse können die Notification API aufrufen, um die Nutzerinteraktion zu erhöhen.

Ein Service Worker kann Netzwerkanfragen von einer Seite abfangen (die ein Abrufereignis auf dem Service Worker auslöst) und eine aus dem Netzwerk abgerufene Antwort zurückgeben, aus einem lokalen Cache abgerufen oder sogar programmatisch erstellt werden. Im Endeffekt ist es ein programmierbarer Proxy im Browser. Das Tolle daran ist, dass die Webseite unabhängig von der Quelle der Antwort so aussieht, als wäre kein Service Worker beteiligt.

Ausführliche Informationen zu Service Workern finden Sie in der Einführung in Service Worker.

Leistungsvorteile

Service Worker sind zwar leistungsstark für das Offline-Caching, bieten aber auch erhebliche Leistungssteigerungen in Form von sofortigem Laden für wiederholte Besuche Ihrer Website oder Webanwendung. Sie können Ihre Anwendungs-Shell im Cache speichern, damit sie offline funktioniert, und ihre Inhalte mit JavaScript füllen.

Auf diese Weise erhalten Sie bei wiederholten Besuchen ohne Netzwerkverbindung aussagekräftige Pixel auf dem Bildschirm, selbst wenn Ihre Inhalte letztendlich von dort stammen. Symbolleisten und Karten werden sofort angezeigt und der Rest des Inhalts wird schrittweise geladen.

Um diese Architektur auf echten Geräten zu testen, haben wir unser Anwendungs-Shell-Beispiel auf WebPageTest.org ausgeführt und sehen die Ergebnisse unten.

Test 1: Test auf Kabel mit Nexus 5 und Chrome Dev

Beim ersten Aufruf der App müssen alle Ressourcen aus dem Netzwerk abgerufen werden.Erst nach 1,2 Sekunden wird eine aussagekräftige Darstellung erzielt. Dank des Service Worker-Caching werden bei unserem wiederholten Besuch aussagekräftige Ergebnisse erzielt und der Ladevorgang ist in 0, 5 Sekunden abgeschlossen.

Diagramm zum Testen der Webseiten-Testfarbe für Kabelverbindung

Test 2: Test in 3G mit Nexus 5 und Chrome Dev

Wir können unser Beispiel auch mit einer etwas langsameren 3G-Verbindung testen. Dieses Mal dauert es beim ersten Besuch 2,5 Sekunden, bis der erste relevante Paint fertig ist. Es dauert 7,1 Sekunden, bis die Seite vollständig geladen ist. Durch das Caching von Service Workern wird bei unserem wiederholten Besuch eine aussagekräftige Anzeige erzielt und der Ladevorgang ist in 0, 8 Sekunden abgeschlossen.

Diagramm zum Webseitentest für die 3G-Verbindung

Andere Datenansichten erzählen eine ähnliche Geschichte. Vergleichen Sie die 3 Sekunden, die benötigt werden, um einen ersten relevanten Paint in der Anwendungs-Shell zu erreichen:

Paint-Zeitachse für den ersten Aufruf durch Web Page Test

auf die 0,9 Sekunden, die benötigt werden, wenn dieselbe Seite aus dem Service Worker-Cache geladen wird. Unsere Endnutzer sparen über 2 Sekunden Zeit.

Paint-Zeitachse für wiederholte Ansicht von Web Page Test

Mit der Anwendungs-Shell-Architektur können Sie ähnliche und zuverlässige Leistungsgewinne für Ihre eigenen Anwendungen erzielen.

Muss der Service Worker überdenken, wie wir Apps strukturieren?

Für Service Worker sind einige geringfügige Änderungen an der Anwendungsarchitektur erforderlich. Anstatt Ihre gesamte Anwendung in einer HTML-Zeichenfolge zu quetschen, kann es von Vorteil sein, Dinge im AJAX-Stil zu tun. Hier haben Sie eine Shell (die immer im Cache gespeichert ist und jederzeit ohne Netzwerk gestartet werden kann) und Inhalte, die regelmäßig aktualisiert und separat verwaltet werden.

Diese Aufteilung hat weitreichende Auswirkungen. Beim ersten Besuch können Sie Inhalte auf dem Server rendern und den Service Worker auf dem Client installieren. Bei nachfolgenden Besuchen müssen Sie nur noch Daten anfordern.

Wie sieht es mit Progressive Enhancement aus?

Service Worker wird zwar derzeit nicht von allen Browsern unterstützt, aber die Shell-Architektur der Anwendungsinhalte nutzt Progressive-Enhancement, um sicherzustellen, dass alle Nutzer auf die Inhalte zugreifen können. Nehmen wir zum Beispiel unser Beispielprojekt.

Unten sehen Sie die Vollversion, die in Chrome, Firefox Nightly und Safari gerendert wird. Ganz links sehen Sie die Safari-Version, bei der der Inhalt ohne Service Worker auf dem Server gerendert wird. Rechts sehen Sie die Nightly-Versionen von Chrome und Firefox Nightly, die vom Service Worker bereitgestellt werden.

Abbildung der in Safari, Chrome und Firefox geladenen Application Shell

Wann ist es sinnvoll, diese Architektur zu verwenden?

Die Anwendungs-Shell-Architektur ist für dynamische Apps und Websites am sinnvollsten. Wenn Ihre Website klein und statisch ist, benötigen Sie wahrscheinlich keine Anwendungs-Shell. Sie können die gesamte Website einfach in einem Service Worker-Schritt oninstall im Cache speichern. Verwenden Sie den Ansatz, der für Ihr Projekt am sinnvollsten ist. Einige JavaScript-Frameworks unterstützen bereits die Aufteilung Ihrer Anwendungslogik vom Inhalt, was die Anwendung dieses Musters einfacher macht.

Gibt es bereits Produktions-Apps, die dieses Muster verwenden?

Die Anwendungs-Shell-Architektur ist mit nur wenigen Änderungen an der Benutzeroberfläche Ihrer gesamten Anwendung möglich. Sie eignet sich gut für große Websites wie die Progressive Web App I/O 2015 von Google und Inbox von Google.

Bild von Google Inbox, das geladen wird. Veranschaulicht Inbox mithilfe eines Service Workers.

Offline-Anwendungs-Shells sind ein großer Leistungsgewinn und werden auch in der Offline-Wikipedia-App von Jake Archibald und der progressiven Web-App von Flipkart Lite gut demonstriert.

Screenshots von Jake Archibalds Wikipedia-Demo

Die Architektur erklären

Beim ersten Laden ist es Ihr Ziel, schnellstmöglich relevante Inhalte auf dem Bildschirm des Nutzers bereitzustellen.

Zuerst andere Seiten laden und laden

Diagramm des ersten Ladevorgangs mit der App Shell

Im Allgemeinen gilt für die Shell-Architektur der Anwendung Folgendes:

  • Priorisieren Sie die anfängliche Last, aber lassen Sie die Anwendungs-Shell vom Service Worker im Cache speichern, damit die Shell bei wiederholten Besuchen nicht noch einmal aus dem Netzwerk abgerufen werden muss.

  • Lazy Loading oder Hintergrundlade alles andere. Eine gute Option ist die Verwendung von Read-through-Caching für dynamische Inhalte.

  • Verwenden Sie Service Worker-Tools wie sw-precache, um den Service Worker, der Ihre statischen Inhalte verwaltet, zuverlässig im Cache zu speichern und zu aktualisieren. Weitere Informationen zu sw-precache erhalten Sie später.

So gehts:

  • Server sendet HTML-Inhalte, die der Client rendern kann. Außerdem werden Header des HTTP-Cache-Ablaufs für die Zukunft verwendet, um Browser ohne Service Worker-Unterstützung zu berücksichtigen. Dateinamen werden mithilfe von Hashes bereitgestellt, um sowohl die Versionsverwaltung als auch einfache Updates für später im Anwendungslebenszyklus zu ermöglichen.

  • Seite(n) werden Inline-CSS-Stile in ein <style>-Tag im Dokument <head> aufnehmen, um eine schnelle First Paint-Methode der Anwendungs-Shell bereitzustellen. Jede Seite lädt das für die aktuelle Ansicht erforderliche JavaScript asynchron. Da CSS nicht asynchron geladen werden kann, können wir Stile unter Verwendung von JavaScript anfordern, da es asynchron und nicht vom Parser gesteuert und synchron ist. Wir können auch requestAnimationFrame() nutzen, um Fälle zu vermeiden, in denen wir einen schnellen Cache-Treffer erhalten und Stile versehentlich Teil des kritischen Rendering-Pfads werden. Mit requestAnimationFrame() wird erzwungen, dass der erste Frame dargestellt wird, bevor die Stile geladen werden. Eine weitere Option ist die Nutzung von Projekten wie loadCSS der Filament Group, um CSS asynchron mit JavaScript anzufordern.

  • Der Service Worker speichert einen im Cache gespeicherten Eintrag der Anwendungs-Shell. So kann die Shell bei wiederholten Besuchen vollständig aus dem Service Worker-Cache geladen werden, sofern im Netzwerk kein Update verfügbar ist.

App-Shell für Inhalte

Eine praktische Umsetzung

Wir haben ein vollständig funktionsfähiges Beispiel mit der Anwendungs-Shell-Architektur, dem Vanilla-ES2015-JavaScript für den Client und Express.js für den Server geschrieben. Natürlich hindert Sie nichts daran, einen eigenen Stack sowohl für den Client als auch für die Serverteile (z. B. PHP, Ruby, Python) zu verwenden.

Service Worker-Lebenszyklus

Für unser Anwendungs-Shell-Projekt verwenden wir sw-precache, das den folgenden Service Worker-Lebenszyklus bietet:

Veranstaltung Aktion
Installieren Speichern Sie die Anwendungs-Shell und andere Single-Page-Anwendungsressourcen im Cache.
Umsetzen Alte Caches löschen
Abrufen Stelle eine einseitige Web-App für URLs bereit und verwende den Cache für Assets und vordefinierte Teile. Netzwerk für andere Anfragen verwenden.

Server-Bits

In dieser Architektur sollte eine serverseitige Komponente (in unserem Fall in Express geschrieben) Inhalt und Präsentation getrennt verarbeiten können. Inhalte können einem HTML-Layout hinzugefügt werden, das zu einer statischen Darstellung der Seite führt, oder sie können separat bereitgestellt und dynamisch geladen werden.

Verständlicherweise unterscheidet sich Ihre serverseitige Einrichtung erheblich von der, die wir für unsere Demo-App verwenden. Dieses Muster von Webanwendungen ist für die meisten Servereinrichtungen erreichbar, erfordert jedoch einige Anpassungen. Das folgende Modell funktioniert recht gut:

Diagramm der App-Shell-Architektur
  • Endpunkte werden für drei Teile Ihrer Anwendung definiert: die an den Nutzer gerichteten URLs (Index/Platzhalter), die Anwendungs-Shell (Service Worker) und Ihre HTML-Teile.

  • Jeder Endpunkt hat einen Controller, der ein Lenker-Layout abruft, das wiederum Teilabschnitte und Ansichten des Lenkers abrufen kann. Einfach ausgedrückt sind Teilansichten Ansichten, die HTML-Chunks sind, die in die endgültige Seite kopiert werden. Hinweis: JavaScript-Frameworks, die eine erweiterte Datensynchronisierung ermöglichen, lassen sich häufig einfacher in eine Application Shell-Architektur portieren. Sie verwenden eher Datenbindung und -synchronisierung als Teilwerte.

  • Dem Nutzer wird zunächst eine statische Seite mit Inhalten angezeigt. Auf dieser Seite wird ein Service Worker registriert, sofern dieser unterstützt wird. Dieser speichert die Anwendungs-Shell und alle Abhängigkeiten (CSS, JS usw.) im Cache.

  • Die App-Shell fungiert dann als eine einseitige Web-App und verwendet JavaScript in XHR im Inhalt für eine bestimmte URL. Die XHR-Aufrufe werden an einen /partials*-Endpunkt gesendet, der den kleinen HTML-, CSS- und JS-Code zurückgibt, der für die Anzeige dieses Inhalts erforderlich ist. Hinweis: Es gibt viele Möglichkeiten, dies zu tun, und XHR ist nur eine davon. Einige Anwendungen fügen ihre Daten für das erste Rendering inline (vielleicht mit JSON) ein und sind daher im Sinne der vereinfachten HTML nicht „statisch“.

  • Für Browser ohne Service Worker-Unterstützung sollte immer ein Fallback bereitgestellt werden. In unserer Demo greifen wir auf das grundlegende statische serverseitige Rendering zurück. Dies ist jedoch nur eine von vielen Optionen. Der Service Worker bietet Ihnen neue Möglichkeiten zur Leistungsoptimierung Ihrer Single-Page-App im Anwendungsstil mithilfe der im Cache gespeicherten Anwendungs-Shell.

Versionsverwaltung für Dateien

Eine Frage, die sich stellt, ist der Umgang mit Dateiversionen und -updates. Dies ist anwendungsspezifisch und die Optionen sind:

  • Netzwerk zuerst und andernfalls die im Cache gespeicherte Version verwenden.

  • Nur Netzwerk und schlagen im Offline-Modus fehl.

  • Alte Version im Cache speichern und später aktualisieren.

Für die Anwendungs-Shell selbst sollte beim Einrichten des Service Workers ein Cache-First-Ansatz gewählt werden. Wenn Sie die Anwendungs-Shell nicht im Cache speichern, haben Sie die Architektur nicht richtig übernommen.

Tools

Wir unterhalten verschiedene Service Worker-Hilfsbibliotheken, die das Precaching der Anwendungs-Shell oder die Handhabung gängiger Caching-Muster vereinfachen.

Screenshot der Service Worker Library-Website in Web Fundamentals

sw-precache für die Anwendungs-Shell verwenden

Wenn Sie die Anwendungs-Shell mit sw-precache im Cache speichern, sollten Probleme in Bezug auf Dateiüberarbeitungen, Fragen zur Installation/Aktivierung und das Abrufszenario für die App-Shell behoben werden. Fügen Sie sw-precache in den Build-Prozess Ihrer Anwendung ein und verwenden Sie konfigurierbare Platzhalter, um Ihre statischen Ressourcen abzurufen. Anstatt Ihr Service Worker-Skript manuell zu erstellen, lassen Sie sw-precache ein Skript generieren, das Ihren Cache sicher und effizient verwaltet, indem Sie einen Cache-First-Abruf-Handler verwenden.

Die ersten Besuche Ihrer Anwendung lösen das Precaching des gesamten Satzes von benötigten Ressourcen aus. Das funktioniert ähnlich wie bei der Installation einer systemeigenen App aus einem App-Shop. Wenn Nutzer zu Ihrer App zurückkehren, werden nur aktualisierte Ressourcen heruntergeladen. In unserer Demo informieren wir Nutzer, wenn eine neue Shell mit der Meldung „App-Updates. Aktualisieren Sie die Seite, um die neue Version zu verwenden." Dieses Muster ist eine unkomplizierte Methode, Nutzer darüber zu informieren, dass sie auf die neueste Version aktualisieren können.

sw-toolbox für das Laufzeit-Caching verwenden

Verwenden Sie sw-toolbox für das Laufzeit-Caching mit unterschiedlichen Strategien je nach Ressource:

  • cacheFirst für Bilder zusammen mit einem dedizierten benannten Cache mit einer benutzerdefinierten Ablaufrichtlinie von N maxEntries.

  • networkFirst oder die schnellste Methode für API-Anfragen. Der schnellste Weg kann in Ordnung sein, aber wenn es einen bestimmten API-Feed gibt, der häufig aktualisiert wird, verwenden Sie networkFirst.

Fazit

Anwendungs-Shell-Architekturen haben mehrere Vorteile, sind aber nur für einige Anwendungsklassen sinnvoll. Das Modell ist noch jung und es wird sich lohnen, den Aufwand und die Gesamtleistung der Architektur zu bewerten.

In unseren Experimenten haben wir die Vorlagenfreigabe zwischen Client und Server genutzt, um den Aufwand der Erstellung von zwei Anwendungsebenen zu minimieren. Dadurch wird sichergestellt, dass die progressive Verbesserung weiterhin eine Hauptfunktion bleibt.

Wenn Sie bereits überlegen, Service Worker in Ihrer App zu verwenden, sehen Sie sich die Architektur an und bewerten Sie, ob sie für Ihre eigenen Projekte sinnvoll ist.

Ein Dank an unsere Prüfer: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage und Joe Medley.