Sofortiges Laden von Webanwendungen mit Anwendungs-Shell-Architektur

Addy Osmani
Addy Osmani
Matt Gaunt

Eine Anwendungs-Shell ist der Mindestumfang an HTML, CSS und JavaScript, der für eine Benutzeroberfläche erforderlich ist. Die Anwendungs-Shell sollte:

  • schnell laden
  • im Cache gespeichert werden
  • Inhalte dynamisch anzeigen

Eine Anwendungs-Shell ist der Schlüssel zu zuverlässig hoher Leistung. Stellen Sie sich die Gehäuse Ihrer App wie ein Code-Bundle vor, das Sie in einem App-Shop veröffentlichen würden, wenn Sie eine native App erstellen. Es ist die Last, die für die Einrichtung erforderlich ist, aber vielleicht ist das nicht alles. Sie speichert Ihre Benutzeroberfläche lokal und ruft Inhalte dynamisch über eine API ab.

App-Shell: Trennung der HTML-, JS- und CSS-Shell und des HTML-Inhalts

Hintergrund

Im Artikel Progressive Web-Apps von Alex Russell wird beschrieben, wie sich eine Web-App nach und nach durch Nutzung und Nutzereinwilligung verändern lässt, um eine native App-ähnliche Nutzererfahrung zu ermöglichen, inklusive Offlineunterstützung, Push-Benachrichtigungen und der Möglichkeit, dem Startbildschirm hinzuzufügen. Sie hängt stark von den Funktionen und Leistungsvorteilen des Service Workers und dessen Caching-Fähigkeiten ab. So können Sie sich auf die Geschwindigkeit konzentrieren und erhalten dieselben sofortigen Ladezeiten und regelmäßigen Updates, die Sie von nativen Anwendungen kennen.

Damit wir diese Funktionen optimal nutzen können, benötigen wir eine neue Denkweise für Websites: die Anwendungsshell-Architektur.

Sehen wir uns nun an, wie Sie Ihre Anwendung mit einer mit Service Worker erweiterten Anwendungs-Shell-Architektur strukturieren. Wir sehen uns das clientseitige und serverseitige Rendering an und zeigen ein End-to-End-Beispiel, das Sie noch heute testen können.

Zur Verdeutlichung des Punkts zeigt das Beispiel unten das erste Laden einer App, die diese Architektur verwendet. Unten auf dem Bildschirm wird die Meldung „Die App ist bereit für die Offlinenutzung“ angezeigt. Wenn zu einem späteren Zeitpunkt ein Update für die Shell verfügbar ist, können wir den Nutzer informieren, dass die neue Version installiert werden muss.

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

Was sind Service Worker?

Ein Service Worker ist ein Skript, das im Hintergrund getrennt von Ihrer Webseite ausgeführt wird. Er reagiert auf Ereignisse, z. B. auf Netzwerkanfragen von Seiten, die er bereitstellt, und Push-Benachrichtigungen von Ihrem Server. Die Lebensdauer eines Service Workers ist bewusst kurz. Er wird aktiviert, wenn ein Ereignis empfangen wird, und wird nur so lange ausgeführt, wie er benötigt, um ihn zu verarbeiten.

Im Vergleich zu JavaScript im normalen Browserkontext haben Service Worker außerdem eine begrenzte Anzahl von APIs. Dies ist der Standard für Worker im Web. Ein Service Worker kann nicht auf das DOM zugreifen, aber beispielsweise auf die Cache API zugreifen. Außerdem kann er Netzwerkanfragen über die Fetch API senden. Die IndexedDB API und postMessage() können ebenfalls für Datenpersistenz und Nachrichtenübermittlung zwischen dem Service Worker und den von ihm gesteuerten Seiten verwendet werden. Push-Ereignisse, die von Ihrem Server gesendet werden, können die Notification API aufrufen, um die Nutzerinteraktion zu erhöhen.

Ein Service Worker kann Netzwerkanfragen von einer Seite abfangen (wodurch ein Abrufereignis auf dem Service Worker ausgelöst wird) und eine Antwort zurückgeben, die aus dem Netzwerk abgerufen oder aus einem lokalen Cache abgerufen oder sogar programmatisch erstellt wird. Es handelt sich im Grunde um einen programmierbaren Proxy im Browser. Das Schöne ist, dass die Webseite unabhängig von der Quelle der Antwort so aussieht, als wäre keine Service Worker-Beteiligung vorhanden.

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

Leistungsvorteile

Service Worker sind für das Offline-Caching leistungsstark, bieten jedoch auch erhebliche Leistungssteigerungen, da sie bei wiederholten Besuchen Ihrer Website oder Webanwendung sofort geladen werden. Sie können Ihre Anwendungs-Shell im Cache speichern, sodass sie offline funktioniert und deren Inhalt mit JavaScript füllt.

Bei wiederholten Besuchen können Sie so auch ohne Netzwerk aussagekräftige Pixel auf dem Bildschirm sehen, auch wenn Ihre Inhalte schließlich von dort stammen. Sie können sich das so vorstellen, als würden Symbolleisten und Karten sofort angezeigt und der Rest Ihres Contents dann nach und nach geladen.

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

Test 1:Kabel testen mit Nexus 5 in Chrome Dev

Die erste Ansicht der App muss alle Ressourcen aus dem Netzwerk abrufen und erreicht erst nach 1,2 Sekunden eine aussagekräftige Palette. Dank des Service Worker-Cachings wird durch unseren wiederholten Besuch eine aussagekräftige Farbe erzielt und der Ladevorgang dauert nur 0, 5 Sekunden.

Farbdiagramm für den Webseitentest für die Kabelverbindung

Test 2:Tests mit 3G mit einem Nexus 5 in Chrome Dev

Wir können das Beispiel auch mit einer etwas langsameren 3G-Verbindung testen. Diesmal dauert es beim ersten Besuch 2,5 Sekunden, bis der erste aussagekräftige Paint angezeigt wird. Es dauert 7,1 Sekunden, bis die Seite vollständig geladen ist. Dank des Service Worker-Cachings wird beim wiederholten Besuch eine aussagekräftige Farbe erzielt und der Ladevorgang ist in 0, 8 Sekunden abgeschlossen.

Farbdiagramm zum Testen der Webseite für die 3G-Verbindung

Andere Ansichten erzählen ein ähnliches Bild. Vergleichen Sie die 3 Sekunden, die für den ersten aussagekräftigen Paint in der Anwendungs-Shell benötigt werden:

Zeitachse für ersten Aufruf über Web Page Test zeichnen

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

Zeitachse für wiederholte Ansicht von Web Page Test zeichnen

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

Müssen wir für Service Worker unsere App-Strukturierung überdenken?

Service Worker erfordern einige subtile Änderungen an der Anwendungsarchitektur. Anstatt Ihre gesamte Anwendung in eine HTML-Zeichenfolge zu quetschen, kann es von Vorteil sein, AJAX-ähnliche Elemente zu verwenden. Hier haben Sie eine Shell (die immer im Cache gespeichert ist und immer ohne Netzwerk starten 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 weiteren Besuchen müssen Sie nur noch Daten anfordern.

Was ist mit Progressive Enhancement?

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

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

Bild der in Safari, Chrome und Firefox geladenen Anwendungs-Shell

Wann ist es sinnvoll, diese Architektur zu verwenden?

Die Anwendungs-Shell-Architektur ist am sinnvollsten für dynamische Anwendungen und Websites geeignet. Wenn Ihre Website klein und statisch ist, benötigen Sie wahrscheinlich keine Anwendungs-Shell und können die gesamte Website einfach in einem Service Worker-Schritt oninstall zwischenspeichern. Verwenden Sie den Ansatz, der für Ihr Projekt am sinnvollsten ist. Einige JavaScript-Frameworks empfehlen bereits, die Anwendungslogik vom Inhalt zu trennen, wodurch dieses Muster einfacher angewendet werden kann.

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

Die Anwendungs-Shell-Architektur lässt sich mit nur wenigen Änderungen an der Benutzeroberfläche Ihrer Anwendung realisieren. Sie funktionierte gut für große Websites wie die progressive Web-App I/O 2015 von Google und den Posteingang von Google.

Bild des Google-Posteingangs wird geladen. Veranschaulicht die Bedienung von Posteingang mithilfe eines Service Workers.

Offline-Anwendungsshells sind ein wichtiger Leistungsgewinn und werden auch in der Offline-Wikipedia-App von Jake Archibald und in der progressiven Web-App von Flipkart Lite gut demonstriert.

Screenshots von Jake Archibalds Wikipedia-Demo

Architektur erklären

Beim ersten Laden ist es Ihr Ziel, so schnell wie möglich aussagekräftige Inhalte auf dem Bildschirm des Nutzers zu sehen.

Zuerst laden und weitere Seiten laden

Diagramm des ersten Ladevorgangs mit der App Shell

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

  • Priorisieren Sie das anfängliche Laden, aber lassen Sie den Service Worker die Anwendungs-Shell zwischenspeichern, damit die Shell bei wiederholten Besuchen nicht erneut aus dem Netzwerk abgerufen werden muss.

  • Alle anderen Inhalte können per Lazy Loading oder im Hintergrund geladen werden. Eine gute Option ist die Verwendung von Lese-Caching für dynamische Inhalte.

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

Gehen Sie dazu so vor:

  • Der Server sendet HTML-Inhalte, die der Client rendern kann, und verwendet Header zum HTTP-Cache-Ablauf in der Zukunft, um Browser ohne Service Worker-Unterstützung zu berücksichtigen. Es stellt Dateinamen mithilfe von Hashes bereit, um sowohl die Versionsverwaltung als auch einfache Updates für später im Anwendungslebenszyklus zu ermöglichen.

  • Seiten enthalten Inline-CSS-Stile in einem <style>-Tag im Dokument <head>, um einen schnellen First Paint der Anwendungs-Shell zu ermöglichen. Auf jeder Seite wird asynchron das für die aktuelle Ansicht erforderliche JavaScript geladen. Da CSS nicht asynchron geladen werden kann, können wir Styles mithilfe von JavaScript anfordern, da es asynchron ist und nicht parsergesteuert und synchron ist. Wir können außerdem requestAnimationFrame() nutzen, um Fälle zu vermeiden, in denen ein schneller Cache-Treffer auftritt und Stile versehentlich Teil des kritischen Rendering-Pfads werden. requestAnimationFrame() erzwingt, dass der erste Frame gezeichnet wird, bevor die Stile geladen werden. Eine weitere Möglichkeit besteht in der Verwendung von Projekten wie loadCSS der Filament Group, um CSS asynchron mit JavaScript anzufordern.

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

App-Shell für Inhalte

Eine praktische Implementierung

Wir haben ein voll funktionsfähiges Beispiel mit der Application Shell-Architektur, Vanilla ES2015 JavaScript für den Client und Express.js für den Server geschrieben. Selbstverständlich hält Sie nichts davon ab, Ihren eigenen Stack für den Client oder die Serverbereiche (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 Vorgang
Installieren Anwendungs-Shell und andere einseitige Anwendungsressourcen im Cache speichern
Aktivieren Leeren Sie alte Caches.
Abrufen Stellen Sie eine einseitige Web-App für URLs bereit und verwenden Sie den Cache für Assets und vordefinierte Teilelemente. Netzwerk für andere Anfragen verwenden.

Server-Bits

In dieser Architektur sollte eine serverseitige Komponente (in unserem Fall in Express geschrieben) in der Lage sein, Inhalt und Präsentation separat zu behandeln. Inhalte können einem HTML-Layout hinzugefügt werden, das zu einem statischen Rendering 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 kann mit den meisten Servereinrichtungen realisiert werden, erfordert jedoch einige Umstrukturierungen. Wir haben festgestellt, dass das folgende Modell ziemlich gut funktioniert:

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

  • Jeder Endpunkt hat einen Controller, der mithilfe eines Handlebars-Layouts Teile des Lenkers und Ansichten abrufen kann. Einfach ausgedrückt sind Teile Ansichten, bei denen es sich um HTML-Blöcke handelt, die in die endgültige Seite kopiert werden. Hinweis: JavaScript-Frameworks, die eine erweiterte Datensynchronisierung ermöglichen, lassen sich oft leichter in eine Application Shell-Architektur übertragen. Sie verwenden tendenziell Datenbindung und -synchronisierung statt Teilabschnitte.

  • Der Nutzer sieht anfangs eine statische Seite mit Inhalten. Auf dieser Seite wird ein Service Worker registriert, der die Anwendungs-Shell und alle Komponenten (CSS, JS usw.) zwischenspeichert, sofern er unterstützt wird.

  • Die App-Shell fungiert dann als einseitige Webanwendung, bei der JavaScript für XHR im Inhalt einer bestimmten URL verwendet wird. Die XHR-Aufrufe erfolgen an einen /partials*-Endpunkt, der den kleinen HTML-, CSS- und JS-Code zurückgibt, der zum Anzeigen dieser Inhalte erforderlich ist. Hinweis: Es gibt viele Möglichkeiten, dieses Problem anzugehen, und XHR ist nur eine davon. Einige Anwendungen fügen ihre Daten beim ersten Rendern inline ein (möglicherweise unter Verwendung von JSON), und sind daher im Sinn der vereinfachten HTML nicht "statisch".

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

Versionsverwaltung für Dateien

Es stellt sich jedoch die Frage, wie mit der Versionsverwaltung und Aktualisierung von Dateien umgegangen wird. Dies ist anwendungsspezifisch. Folgende Optionen stehen zur Verfügung:

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

  • Nur Netzwerkverbindung und Fehler, wenn offline.

  • Speichern Sie die alte Version im Cache und aktualisieren Sie sie später.

Für die Anwendungs-Shell selbst sollten Sie für die Einrichtung des Service Workers einen Cache-First-Ansatz verfolgen. Wenn Sie die Anwendungs-Shell nicht im Cache speichern, haben Sie die Architektur nicht richtig übernommen.

Tools

Wir haben eine Reihe verschiedener Service Worker-Hilfsbibliotheken, die das Pre-Caching der Shell Ihrer Anwendung oder die Verarbeitung gängiger Caching-Muster einfacher einrichten.

Screenshot der Service Worker Library Site on Web Fundamentals

„sw-precache“ für die Anwendungs-Shell verwenden

Wenn Sie sw-precache verwenden, um die Anwendungs-Shell im Cache zu speichern, sollten Bedenken in Bezug auf Dateiüberarbeitungen, die Fragen zum Installieren/Aktivieren und das Abrufszenario für die Anwendungs-Shell gelöst 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 mithilfe eines Cache-First-Fetch-Handlers ein Skript generieren, das Ihren Cache sicher und effizient verwaltet.

Bei ersten Besuchen Ihrer App wird das Pre-Caching der gesamten benötigten Ressourcen ausgelöst. Dies ist vergleichbar mit der Installation einer nativen App aus einem App-Shop. Wenn Nutzer zu Ihrer App zurückkehren, werden nur aktualisierte Ressourcen heruntergeladen. In unserer Demo informieren wir die Nutzer mit der Nachricht „App-Updates. Aktualisieren Sie die Seite, um die neue Version zu sehen.“ Dieses Muster ist eine unkomplizierte Methode, um Nutzende darüber zu informieren, dass sie auf die neueste Version aktualisieren können.

Sw-Toolbox für Laufzeit-Caching verwenden

Verwenden Sie sw-toolbox für das Laufzeit-Caching mit unterschiedlichen Strategien, die von der Ressource abhängen:

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

  • networkFirst oder am schnellsten für API-Anfragen, je nach gewünschter Inhaltsaktualität. Schnellstes Vorgehen kann in Ordnung sein, aber wenn es einen bestimmten API-Feed gibt, der häufig aktualisiert wird, sollten Sie networkFirst verwenden.

Fazit

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

In unseren Experimenten haben wir die gemeinsame Nutzung von Vorlagen zwischen Client und Server genutzt, um den Arbeitsaufwand für die Erstellung von zwei Anwendungsebenen zu minimieren. Dadurch wird sichergestellt, dass Progressive Enhancement weiterhin eine Kernfunktion ist.

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

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