,

ServiceWorker – Offline First

Benedikt Reuter

In der Vorlesung Rich Media haben wir uns viel mit Performance in Web Anwendungen beschäftigt. Dabei habe ich mich mit ServiceWorkern in Bezug auf Offlinenutzung, Funktionalität und Performance beschäftigt. Zuerst habe ich mich damit befasst, wie ein ServiceWorker funktioniert. Danach habe ich geschaut, wie sich die Nutzung eines ServiceWorker und des Ansatzes Offline First auf die Performance auswirkt.

Was sind ServiceWorker?

ServiceWorker bieten einige hilfreiche Funktionalitäten. Mit ihrer Hilfe können zum Beispiel Webseiten völlig Offline bedienbar gemacht werden. Zusätzlich kann man das Caching komplett selbst in die Hand nehmen und konfigurieren, wie man es selbst für richtig hält. Ein ServiceWorker läuft im Hintergrund der Webseite, daher ist es möglich im Hintergrund Daten zu synchronisieren und Push Notifications zu versenden. Durch die Synchronisation im Hintergrund kann man auch während einer Offline Session http Requests abspeichern und absenden, sobald man wieder Online ist.

Technisch gesehen ist ein ServiceWorker ein ganz einfacher programmierbarer Netzwerk Proxy im Browser. Er basiert auf den Event-getriebenen Web Workern und läuft in einem separaten Thread des Browsers. Sobald ein ServiceWorker mal nicht gebraucht wird, wird er beendet. Wenn er dann wieder benötigt wird, wird er wieder gestartet. Dadurch werden keine Ressourcen dauerhaft verbraucht. Er kann auch laufen, wenn die zugehörige Webseite nicht geöffnet ist, z.B. um Push Notifications zu senden.
Da man mit dem ServiceWorker alle Requests abfängt und abändern kann, wird aus Sicherheitsgründen nur https erlaubt. Während der Entwicklung unter localhost kann jedoch trotzdem http genutzt werden. Ein ServiceWorker kann nicht auf den DOM zugreifen, daher wird über eine Schnittstelle kommuniziert. Genauer gesagt über EventListener und die postMessage Schnittstelle. Diese Eigenschaften sind durch die Basis des Web Workers gegeben.

Hauptsächlich genutzt werden ServiceWorker in Progressive Web Apps, wobei sie auch auf vielen anderen Webseiten zum Einsatz kommen. Progressive Web Apps werden jedoch bereits in einem anderen Blogeintrag innerhalb dieser Vorlesung beschrieben, daher werde ich hier nicht weiter darauf eingehen.

Unterstütz werden ServiceWorker mittlerweile von den meisten Browsern. Selbst die meisten mobilen Browser haben keine Probleme damit. Progressive Web Apps mit einem ServiceWorker kommen sehr nah an eine App ran, die Möglichkeit eine App damit abzulösen liegt also nicht so weit entfernt.

LifeCycle Events

Hier wird das ganze etwas Technischer. Im Leben eines ServiceWorker gibt es  drei wichtige Lifecycle Events: register, install und activate. Durch diese Events muss der ServiceWorker gehen, bevor er genutzt werden kann.

register

Damit der Browser von dem ServiceWorker erfährt, muss er beim ihm registriert werden. Dies passiert innerhalb des Scripts der Webseite.

In Zeile 1 wird erst einmal überprüft, ob der ServiceWorker überhaupt vom Browser unterstützt wird. Falls nicht, kann die Webseite benutzt werden wie immer.  Die if-Bedingung in Zeile 2 wartet auf das „load“ Event. Damit erreicht man, dass der ServiceWorker erst verzögert startet. Der ServiceWorker wird so erst nach dem vollständigen Laden der Webseite initialisiert und dem Benutzer entstehen so keine unnötigen Ladezeiten. Wenn man den ServiceWorker jedoch von Anfang an benötigt, kann man dies aber auch weglassen.

Ab Zeile 4 wird der ServiceWorker registriert. Dabei kann, wie in Zeile 6 beschrieben der Scope definiert werden. In diesem Fall wird der ServiceWorker alle Requests mit der URL „/app“ betrachten. Dazu zählen „/app“, „app/test“ oder „/app/test/test“. Eine Ebene weiter oben wird jedoch nicht beachtet, z.B. „/“ oder „/test“. Wenn die Scope Variable weggelassen wird, ist der Ordner in dem der ServiceWorker liegt der entsprechende Scope.

Install

Nachdem der ServiceWorker im Browser registriert wurde, muss er installiert werden. In diesem Schritt können benötigte Caches bereits aufgebaut werden. Dies kann auch mal etwas mehr sein, da hier auch größere Mengen an statischen Dateien runtergeladen werden können. Hier könnten eventuell größere Ladezeiten entstehen, wenn man nicht auf das vollständige warten Laden der Webseite warten würde.

Damit der erstellte Cache klar identifizierbar ist, wird zuerst ein Name vergeben. Dieser kann sich in späteren Iterationen des ServiceWorkers ändern, um den Cache zu erneuern. In der zweiten Zeile werden Ressourcen angegeben, welche im späteren Verlauf heruntergeladen werden sollen. Dies sind meistens statische Dateien, welche man immer gecached bzw. Offline verfügbar haben möchte. In Zeile 8 wird auf das Durchführen aller Installationsschritte gewartet. In diesem Fall gibt es nur einen Schritt, das Runterladen der Ressourcen. Es wird der vorher definierte Cache geöffnet und dann alle definierten Ressourcen diesem Cache hinzugefügt. Dadurch sind sie später im laufenden Betrieb verfügbar. Falls auch nur einer der durchgeführten Installationsschritte fehlschlägt, wird der ServiceWorker verworfen und es wird später eine erneute Installation gestartet. Wenn also eine Ressource nicht verfügbar ist, wird die Installation nicht funktionieren.

Sobald alle Installationsschritte fertiggestellt wurden ist der Worker bereit, um genutzt zu werden. Auf das Aktivieren muss er eventuell trotzdem noch warten. Beim ersten Öffnen der Webseite übernimmt er die Webseite einfach, das Warten ist also nicht notwendig. Wenn die Webseite jedoch bereits von einem ServiceWorker kontrolliert wurde, muss er warten, bis alle kontrollierten Webseiten geschlossen wurden. Dies kann auch mal etwas länger dauern. Um dieses Warten zu überspringen, kann self.skipWaiting() aufgerufen werden, wie in Zeile 14. Damit wird der alte ServiceWorker entfernt und durch den neuen ersetzt. Man sollte damit aber vorsichtig sein, hier könnten Daten verloren gehen. Wenn man mit sensiblen Daten arbeitet, ist es das vermutlich nicht wert.

activate

Sobald der ServiceWorker die Kontrolle über die Webseite übernommen hat, wird das activate Event ausgelöst. Hier ist der richtige Zeitpunkt, um alle veralteten Caches aufzuräumen. Im Installationsschritt wird der Cache eventuell noch von einem vorherigen ServiceWorker genutzt. Im activate Schritt kann man jedoch davon ausgehen, dass kein älterer ServiceWorker auf die Caches zugreifen wird.

Im Beispiel ist wieder der Cache Name relevant. In diesem Beispiel sollten im Installationsschritt alle statischen Dateien im Cache „v5“ abgespeichert werden, da hier alle anderen Caches gelöscht werden. Wenn sich also der Name des Caches ändert, werden alle alten Versionen gelöscht.

LifeCycle Überblick

Nochmal ein kleiner Überblick über alle Events:

Im register wird der ServiceWorker im Browser registriert. Dann wird das install Event ausgelöst. Hier werden alle benötigten Caches aufgebaut. Daraufhin wird gewartet, bis das activate Event ausgelöst werden kann. Dies passiert sobald die alte Version des ServiceWorkers die Webseite freigibt bzw. sofort, wenn dies die erste Version ist. Im activate Event können alte Caches aufgeräumt werden. Nachdem dieses Event abgeschlossen wurde, ist der ServiceWorker einsatzbereit. Er wird jedoch beendet, bis er wieder benötigt wird.

fetch

Nach den LifeCycle Events kommen wir nun zum vermutlich wichtigsten Teil des ServiceWorker, dem fetch Event. In diesem Event wird jeder http Request abgefangen. Hier können Caching und Offline Nutzung implementiert werden. Um das ganze möglichst Verständlich zu machen, werde ich in den nächsten Beispielen das fetch Event immer erweitern.

Der einfachste EventListener für das fetch Event ist der Folgende:

e ist das Event, und mit e.request kann man auf den entsprechenden Request zugreifen. In diesem Beispiel wird nichts mit dem Request gemacht. Er wird mit der Methode fetch() an das eigentliche Ziel weitergeleitet und die Antwort wird mit e.respondeWith() zurückgegeben.

Sobald es einen solchen fetch EventListener gibt, werden alle Request durch diesen gehen. Wenn diese Funktionalität des ServiceWorkers nicht benötigt wird, kann dieser Listener auch einfach weggelassen werden. Damit spart man sich den Zwischenschritt bei jedem Request.

Der nächste Schritt ist eine einfache Offline Funktionalität für alle gecachten Inhalte:

Jetzt wird immer, wenn ein Request über das Netzwerk fehlschlägt, nach einer passenden Antwort im Cache geschaut. Wenn diese vorhanden ist, wird sie zurückgegeben. Falls dies nicht der Fall ist, wird die normale Fehlerseite durch den Browser angezeigt. Hier könnte man aber auch eine Standard Antwort für einen Fehler definieren, welche man immer bei einer fehlenden Netzwerkverbindung und fehlender Ressource im Cache zurückgeben kann.  Hier im Beispiel werden noch keine Requests gespart, da nur im Cache geschaut wird, wenn der eigentliche Netzwerkzugriff fehlgeschlagen ist. Außerdem werden nur Ressourcen im Cache vorhanden sein, welche bereits im Installationsschritt runtergeladen wurden.

Im folgenden Beispiel wird der Zugriff auf den Cache vor den Netzwerkzugriff geschoben, damit man den Netzwerkzugriff auf die bereits gespeicherten Ressourcen spart. Falls aber ein Netzwerkzugriff getätigt werden muss, wird die Antwort davon im Cache gespeichert.   

In Zeile 3 bis 6 wird geschaut, ob eine Antwort zu dem gegebenen Request vorhanden ist. Falls dies nicht der Fall ist, wird ab Zeile 7 der Netzwerkaufruf gemacht. Falls dieser erfolgreich war (Zeile 8), wird die Antwort zuerst kopiert (Zeile 10) und dann in den Cache geschrieben (Zeile 11). Am Ende wird die Antwort dann zurückgegeben (Zeile 14). Damit wird dieser Request das nächste Mal im Cache vorhanden sein und ein erneuter Netzwerkzugriff gespart. Der Cache baut sich so automatisch aus und wird mit jedem neuen Request größer.

Jetzt fehlt z.B. noch eine Strategie, um die Ressourcen nach einer gewissen Zeit ablaufen zu lassen. Da man vollständigen Zugriff auf den Request hat, kann man jedoch auch selbst entscheiden, ob gewisse Ressourcen überhaupt erst im Cache gespeichert werden sollten. An dieser Stelle werde ich jedoch keine weiteren Beispiele zeigen, da dies nur indirekt die Performance beeinflussen würde.

sync

sync ist das nächste Event auf der Tagesordnung. Dieses Event ermöglicht eine Synchronisierung von Daten im Hintergrund. Außerdem ist es möglich, im Offlinemodus ein sync Event zu starten, welches dann durchgeführt wird, sobald man wieder eine funktionierende Netzwerkverbindung hat.

Hier sieht man wie ein solches Event ausgelöst wird. In Zeile 2 wird zuerst eine Instanz des registrierten ServiceWorker geholt. In Zeile 3 löst man dann ein sync Event mit dem Namen „extendCache“ aus. Im ServiceWorker kommt dieses Event im sync EventListener an. Diese Synchronisation soll jetzt, wie der Name bereits verrät den Cache erweitern.

Im EventListener wird zuerst der entsprechende sync mit einer if Anweisung gefiltert, in unserem Beispiel suchen wir nach dem sync mit dem Tag „extendCache“ (Zeile 2). Dann wird der Cache geöffnet und die Ressource „about.html“ hinzugefügt (Zeile 4-7). „about.html“ ist ab jetzt offline verfügbar. Die Besonderheit dabei ist, dass dieses Event im Offlinemodus ausgelöst werden kann und ausgeführt wird, sobald der Browser wieder online ist.
Über diesen Weg kann man sehr gut E-Mails oder Nachrichten versenden. Selbst wenn man zum aktuellen Zeitpunkt keine aktive Netzwerkverbindung hat, wird die Nachricht versendet, sobald der Browser wieder online ist.

Der ServiceWorker hat zwar noch einige weitere Funktionen, ich habe aber versucht nur die wichtigsten Funktionen bezüglich des Themas zu behandeln.

Funktionalität und Performance

Wie bereits erwähnt, wird jeglicher Netzwerkverkehr durch den ServiceWorker geleitet. Es ist also offensichtlich, dass dort eine kleine Verzögerung entsteht. Wenn man also nur eine Offlinefunktionalität einbauen möchte, wird man automatisch minimale Performance Verschlechterungen haben. Diese sollte aber durch intelligentes Caching recht leicht wieder ausgleichbar sein.

Zu einem Vor- und Nachteil könnte werden, dass der Entwickler komplett eigenverantwortlich das Caching umsetzen kann bzw. muss. Hier können einige Fehler passieren, welche die Performance verschlechtern könnten. Auf der anderen Seite sollte der Entwickler aber auch am besten wissen, welche Ressourcen gecached werden können und welche nicht. Man könnte z.B. Templates für die App komplett im Cache behalten und nur Inhalte innerhalb dieser Templates über das Netzwerk abrufen. Die Grundstrukturen der Webseite bleiben in der Regel ja immer die gleichen, nur die Inhalte ändern sich. Zusätzlich dazu hat man dann die Offline Funktion, welche zusätzlich auch die Inhalte der letzten Online Requests bereitstellen kann.

Ein weiteres Problem ist der Start des ServiceWorkers. Er wird immer terminiert, sobald er nicht gebraucht wird. Dies führt dazu, dass er auch immer wieder neu gestartet werden muss, bevor er wieder arbeiten kann. Dadurch gibt es eine kleine Verzögerung, bis er dem Besucher der Webseite Ressourcen zu Verfügung stellen kann. Um dieser Verzögerung entgegenzuwirken, wurde die Strategie „Navigation Preload“ entwickelt. Es können bereits parallel zum Start eines ServiceWorkers entsprechende Requests durchgeführt werden, damit diese bei Bedarf innerhalb des fetch Events genutzt werden können. Dies führt aber dazu, dass eventuell unnötige Netzwerkzugriffe durchgeführt werden, da der Zugriff bei einem Treffer im Cache komplett umsonst war. Aber bei Geräten mit schlechterer Hardware, kann man so etwas Zeit einsparen.

Gerade in der heutigen Zeit, wo auch der Speicher auf mobilen Geräten immer größer wird, könnten ServiceWorker eine gute Hilfe sein. Durch das großzügige Caching können viele Funktionalitäten auch Offline verfügbar gemacht werden. Bei Geräten mit wenig Speicher ist dies natürlich problematisch. Die gespeicherten Seiten werden auf Dauer vermutlich zu viel Speicher benötigen.

Datenschutz

Wenn man aktuell von Webtracking hört, denkt man sofort an Cookies. ServiceWorker können aber im Prinzip ähnliche Dinge erreichen. Sie können Daten über Requests sammeln oder im Hintergrund Daten synchronisieren. Zusätzlich weiß man oft nicht, dass eine Webseite einen ServiceWorker einsetzt.

Um zu überprüfen, welche Webseiten einen ServiceWorker benutzen kann man in Chrome alle genutzten ServiceWorker unter folgender URL einsehen: „chrome://serviceworker-internals“. In Firefox kann man dies unter der URL „about:debugging“ tun.

Auch die Transparenz bei ServiceWorkern lässt zu wünschen übrig. Viele Webseiten benutzen ServiceWorker, man weiß nur nichts davon. Zum einen kann man dadurch die Funktionalitäten nicht optimal nutzen, zum anderen weiß man dann aber auch nicht, dass dort Daten gespeichert werden können.

Fazit

Schlussendlich würde ich sagen, dass ein ServiceWorker durchaus sehr gute Vorteile bringen kann. Man sollte aber nicht denken, dass man direkt nach dem Einsatz eine bessere Performance und ein optimales Caching hat. Ein unbedachter Einsatz kann auch schnell kontraproduktiv sein.

Die verschiedenen beschriebenen Konfigurationen eines ServiceWorker habe ich getestet und auch die Performance überprüft. Dabei sind mir kaum Unterschiede aufgefallen, vermutlich weil meine Testanwendung zu klein und nicht komplex genug war. Der standardmäßige Caching Mechanismus im Browser war in allen Fällen gleich gut. Um klarere Ergebnisse erzielen zu können, müsste man meiner Meinung nach zum einen eine größere Anwendung testen und zum anderen klare Ziele setzen.
Durch das individuelle Caching setzt der Mechanismus teilweise nur an anderen Stellen an. So wird die Performance nicht verbessert oder verschlechtert, sondern die Arbeiten nur verschoben. Dies macht die Performance-Überprüfungen etwas schwieriger. Ein ServiceWorker ermöglicht es jedoch relativ einfach, dass man den Aufruf von weiteren Seiten der Webseite beschleunigt. Dies kann man erreichen, indem man bereits während der Installation oder während eines „ruhigen“ Moments im Hintergrund, Daten runterlädt.  

Mein Ziel war es mehr oder weniger herauszufinden, ob die Anwendung durch den Offline First Ansatz an Performance verliert. Dies ist nach meinen Untersuchungen nicht deutlich geworden.

Referenzen


Posted

in

,

by

Benedikt Reuter

Tags:

Comments

Leave a Reply