
Teilen Sie:
Yonatan war an einigen großartigen Projekten in der Akademie und in der Industrie beteiligt - von C/C++ über Matlab bis hin zu PHP und Javascript. Früher war er CTO bei Webiks und Softwarearchitekt bei WalkMe. Derzeit ist er Softwarearchitekt bei Vonage und egghead-Dozent.
Wie Javascript Service Workers die Geschwindigkeit unserer Website um 97,5 % erhöht hat
Lesedauer: 7 Minuten
Hier erfahren Sie, wie wir unsere Website durch den Einsatz von Service Workern um 97,5 % beschleunigt haben, wie wir sicherstellen, dass die Benutzer jedes Mal die neueste Version erhalten, und wie Sie es auch tun können.
Die Website des Designsystems von Vivid hat in nur einem Jahr einen langen Weg zurückgelegt. Sie begann mit ein paar einfachen Seiten und hat sich nun zu einer vollwertigen Dokumentations-Website entwickelt, auf der Live-Beispiele für die Verwendung unseres Produkts gezeigt werden.
Wir verwenden Code-Splitting ausgiebig in unserem Code, und unser Produkt enthält verschiedene statische Ressourcen wie Icons und CSS-Dateien. Darüber hinaus verwendet unsere Dokumentation iFrames, um unsere Komponenten zu präsentieren, wobei jede ihre Abhängigkeiten separat aufruft (ja, wie eine Microfrontend-Architektur).
Doch im Laufe der Zeit begannen die Dinge zu zerbrechen.
Es begann mit unserer Entwicklungsumgebung. Nach ein paar Neuladungen blieb es einfach stecken.
vivid-docs-loading-error.gif
Damit konnten wir leben. Womit wir nicht leben konnten, war ein Ticket, das von unseren Nutzern kam und uns zeigte, wie schlimm die Situation wirklich war.
vivid-user-ticket.png
Wir wussten, dass wir etwas dagegen tun mussten. Wir mussten unsere Website beschleunigen. Im Folgenden erfahren Sie, wie wir die Leistung unserer Website überprüft und verbessert haben.
Der Leistungsschub
Fangen wir vom Ende her an. Unser Hauptziel war es, die Anzahl der Aufrufe an den Server zu reduzieren. Ein sekundäres Ziel (oder ein Bonus, wenn Sie so wollen) war die Beschleunigung der Ladezeit von Seiten in unserer App. Sie können die Ergebnisse sehen und selbst beurteilen:
Vor
vivid-performance-before.png
In der obigen Abbildung ist das Profil der Netzwerkanfragen vor der Änderung zu sehen. An einigen Stellen dauerte das Laden der Seite 860 ms - eine enorme Zeitspanne! Die durchschnittliche Ladezeit einer Seite lag bei 400 ms. Ganz rechts sehen Sie den grauen Balken, der anzeigt, dass die Seite versucht, die all.css Datei zu laden, was ewig dauert. Dies führt dazu, dass die Seite nicht mehr geladen wird, und wenn der Benutzer versucht, die Seite zu aktualisieren, wird die Situation noch schlimmer!
vivid-performance-after.png
Wie haben wir es also geschafft?
Problem: Das Chrome-Verbindungslimit
Chrome bietet ein Limit von 6 gleichzeitigen Verbindungen zu einem Server. Was uns überraschte, war der lange Timeout und dass die Anfragen auch dann "live" blieben, wenn wir die Seite aktualisierten oder eine andere Seite aufriefen.
Da wir fast hundert Anfragen auf jeder Seite hatten (wir haben den Code extrem gesplittet), beendete Chrome die Verbindung zu unserer Website für jeden Benutzer nach nur wenigen Minuten des Surfens. Wir hatten also eine Aufgabe: die Anzahl der Dateien zu reduzieren, die Chrome laden musste.
Problem: Zu viele Dateien
Versuch #1: Prefetch entfernen
Wie waren wir sicher, dass wir zu viele Dateien hatten? Als wir den Chrome Inspector öffneten, sah unser Code wie folgt aus:
chrome-inspector-before-service-workers.png
Diese Dateien sind nur ein kleiner Teil unseres Dateimanifests. Sehen Sie, dass wir die Dateien auch vorladen, um die Ladezeit der folgenden Seiten zu beschleunigen.
Unser erster Schritt bestand darin, den Prefetch zu stoppen. Dies verringerte zwar die Anzahl der Anfragen, half aber nicht, da der Prefetch nur dann stattfand, wenn das Netzwerk im Leerlauf war, so dass es keine große Wirkung hatte. Wir mussten die Anzahl der angeforderten Skripte verringern.
Versuch Nummer 2: Bündeln Sie alle Dateien zu einem großen Bündel
In unserem Projekt verwenden wir rollup um unsere Dateien zu bündeln. Unsere Konfiguration ist so ausgelegt, dass alles kodiert aufgeteilt wird und die Verbraucher ihre eigenen Bundler verwenden, um kodiert aufzuteilen, zu bündeln und den Baum zu schütteln.
In diesem Schritt sind wir einfach alle Komponenten durchgegangen, haben eine Barrel-Datei erstellt und sie alle in einer großen "vivid-components.js"-Datei gebündelt, die wir anstelle aller anderen Skript-Tags verwendet haben. Schauen Sie sich diese Übergabe um zu sehen, wie wir das gemacht haben.
Bei den meisten Seiten hat es geholfen, aber - erinnern Sie sich an die iFrames? Sie brachten immer noch eine Menge Zeug - Duplikate der Dateien, die bereits von der ersten Seite gebracht wurden.
Dies brachte uns zu unserer letzten und effektivsten Lösung: Servicekräfte.
Lösung: Dienstleistende
Ein Service Worker ist eine Schicht zwischen unserer Anwendung und dem Netzwerk. Er kann alle Anfragen abhören, die in der App ein- und ausgehen, und sie bearbeiten.
In unserem Fall wollten wir die Anfragen bearbeiten, die Antwort zwischenspeichern und die Antwort auf nachfolgende Anfragen zurückgeben.
Wie man einen Service Worker registriert
Der erste Schritt ist die Registrierung des Service Workers im Client:
(async function() {
const registration = await navigator.serviceWorker.register(
'/sw.js',
{
scope: '/',
}
);
})();Der Service Worker kann Anfragen entsprechend dem Ordner, der ihn enthält, abrufen. Aus diesem Grund habe ich ihn in das Stammverzeichnis meines Projekts gelegt.
In unserem Projekt befindet sich die eigentliche Datei nicht im Stammverzeichnis. Sie wird während des Build-Prozesses dorthin verschoben, was uns eine angenehme Entwicklungsumgebung bietet, während wir weiterhin Anfragen aus dem Stammverzeichnis abrufen können. Sie könnten die service-worker-allowed http-header verwenden, um die Datei in einem anderen Ordner bereitzustellen, aber mit dem "build to root"-Trick hatten wir keine Notwendigkeit dafür.
Service-Worker-Funktionalität
Unser Service-Mitarbeiter sieht folgendermaßen aus:
const addResourcesToCache = async (resources) => {
const cache = await caches.open('vivid-cache');
await cache.addAll(resources);
};
const putInCache = async (request, response) => {
const cache = await caches.open('vivid-cache');
await cache.put(request, response);
};
const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
const preloadResponse = await preloadResponsePromise;
if (preloadResponse) {
console.info('using preload response', preloadResponse);
await putInCache(request, preloadResponse.clone());
return preloadResponse;
}
try {
const responseFromNetwork = await fetch(request);
await putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
} catch (error) {
const fallbackResponse = await caches.match(fallbackUrl);
if (fallbackResponse) {
return fallbackResponse;
}
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
});
}
};
const enableNavigationPreload = async () => {
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
};
self.addEventListener('activate', (event) => {
event.waitUntil(enableNavigationPreload());
});
self.addEventListener('install', (event) => {
event.waitUntil(
addResourcesToCache([
'./',
'./index.html',
'/assets/styles/core/all.css',
'/assets/scripts/vivid-components.js',
'/assets/scripts/live-sample.js',
])
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
cacheFirst({
request: event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: './assets/images/vivid-logo.jpeg',
})
);
});
Wir haben zwei Hilfsfunktionen: addResourcesToCache zum Hinzufügen einer Ressource zum Cache und putInCache um eine Anfrage und ihre Antwort in den Cache zu stellen.
Sie verwenden beide die Caches globales Objekt, das uns den Zugriff auf den CacheStorage Objekt ermöglicht.
cacheFirst ist der Ort, an dem die Magie geschieht. Es wird versucht, die Antwort aus dem Cache zu holen. Wenn sie gefunden wird, wird die Antwort aus dem Cache zurückgegeben. (Zeilen 12-15)
Wenn nicht, wird versucht, die Antwort von einem Preload zu erhalten. Wenn es funktioniert, ist alles in Ordnung - wir zwischenspeichern es und geben die vorgeladene Antwort zurück. (Zeilen 17-22)
Schlägt dies fehl, wird eine Anfrage an das Netz (z. B. den Server) gestellt, die Antwort abgerufen und zwischengespeichert. (Zeilen 24-27)
Wenn alles fehlschlägt, geben wir einfach einen Fehler mit einem Bild zurück. (Zeilen 29-35)
Lebenszyklus eines Service Workers
Der Lebenszyklus des Service Workers:
Registrierung (das haben wir schon hinter uns)
Einrichtung
Freischaltung
Unser Service Worker hört die Installationsphase ab und fügt dem Cache unsere wichtigsten Ressourcen hinzu.
self.addEventListener('install', (event) => {
event.waitUntil(
addResourcesToCache([
'./',
'./index.html',
'/assets/styles/core/all.css',
'/assets/scripts/vivid-components.js',
'/assets/scripts/live-sample.js',
])
);
});
Beachten Sie den Nutzen waitUntil das wir für das Ereignisobjekt erhalten. Dieses Dienstprogramm hilft uns, Race Conditions zu vermeiden, da es auf den Abschluss asynchroner Operationen wartet.
Dann, in der activate Phase wird das Vorladen von Inhalten aktiviert (mit waitUntil).
const enableNavigationPreload = async () => {
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
};
self.addEventListener('activate', (event) => {
event.waitUntil(enableNavigationPreload());
});
Der letzte Schritt ist das Hinzufügen eines Hörers zu fetch. Dieser Listener fängt die Anfragen ab und ermöglicht es uns, sie mit unserer cacheFirst Funktion bearbeiten:
self.addEventListener('fetch', (event) => {
event.respondWith(
cacheFirst({
request: event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: './assets/images/vivid-logo.jpeg',
})
);
});
Beachten Sie die respondWith Dienstprogramm. Es tut wortwörtlich genau das, was es sagt - angesichts der Anfrage können wir eine beliebige Antwort zurückgeben. In diesem Fall geben wir das Ergebnis von cacheFirst.
Handhabung von Versionen in einem Service Worker
Es kann vorkommen, dass Sie die Version eines Service Workers aktualisieren möchten. In unserem Fall ist sie für die Aktualisierung unserer Bibliothek erforderlich. Dazu müssen wir die Version in der Datei unseres Service Workers angeben, einen versionierten Cache erstellen und den alten Cache löschen.
Wie man einen versionierten Cache in einem Service Worker erstellt
Das Hinzufügen einer Version ist ganz einfach: const VERSION = ‘3.17.0’;
Dies kann bei jeder Veröffentlichung manuell geändert werden.
In unserem Projekt verwenden wir zum Beispiel Rollup zum Bündeln, also haben wir den folgenden "Trick" angewandt:
Wir stellen die Version so ein:
const VERSION = ‘SW_VERSION’;Während des Builds extrahieren wir die Version aus unserem
package.jsonWir verwenden das Replace-Plugin des Rollups, um die Version bei jedem Build zu setzen.
Sie können sich unsere Einrichtung hier.
Wie man einen versionierten Cache in einem Service Worker erstellt
Wie Sie bemerkt haben, akzeptiert die caches.open Methode eine Zeichenkette akzeptiert:
const cache = await caches.open(VERSION);
Er erwartet einen Cache-Namen oder eine ID, auf die wir später verweisen können.
Oft müssen Sie den Service Worker oder den Cache Ihrer Website aktualisieren. Zum Beispiel, wenn Sie eine Bibliotheksversion ändern, der Seite einen neuen Abschnitt hinzufügen und jede Änderung, die Sie an der Antwort vornehmen, anzeigen möchten, ohne den Ablauf des Caches abzuwarten.
Die Verwendung der Version als ID ermöglicht es uns, den Cache jeder Version anzusprechen und so die aktuelle Version anzuzeigen und alte zu löschen.
Löschen von veraltetem Cache in einem Service Worker
Nun, da wir den Cache versioniert haben, können wir ihn löschen.
Lassen Sie uns eine Funktion erstellen removeOldCache:
async function removeOldCache(event) {
await caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (key) {
return key !== VERSION;
}).map(function (key) {
return caches.delete(key);
}));
}).then(function () {
return self.clients.claim();
});
}Die Funktion geht alle Cache-Schlüssel durch (Zeile 2), und für jeden Schlüssel, der nicht die neue aktivierte Version ist, wird er gelöscht (Zeile 6). Sobald dies erledigt ist, verwenden wir die self.clients.claim Methode, um dem Browser mitzuteilen, dass unser neuer Service Worker jetzt alle Registerkarten kontrolliert (Zeile 9).
Wir rufen diese Funktion während der Aktivierung auf:
self.addEventListener('activate', (event) => {
event.waitUntil(removeOldCache(event));
event.waitUntil(enableNavigationPreload());
});
Beachten Sie, dass der Cache der globale Cache für alle Service Worker ist. In unserem Fall können wir sie sicher entfernen, aber in Ihrem Fall haben Sie vielleicht mehr als einen Cache und könnten daher erwägen, ein Suffix an die Version zu hängen, um nur den gewünschten Cache zu entfernen.
Ein Beispiel ist ein Fall, in dem Sie den HTML-Cache und den Cache für Serveranfragen trennen möchten. Der HTML-Cache wird geleert, wenn Sie clientseitige Bereiche ändern, während der Cache für Serveranfragen von einem anderen Begriff abhängt.
Aktualisierung der Service Worker
Neue Service Worker haben eine Wartezeit. Es kann ein paar Stunden dauern, bis sie ersetzt werden. Wir können diese Wartezeit überspringen, indem wir self.skipWaiting() in unserer Installationsphase einfügen.
Jetzt wird unser Service Worker sofort aktualisiert, wenn wir ihn ändern (z. B. eine neue Version der Bibliothek hochladen).
Zusammenfassung
Service Workers sind ein sehr mächtiges Werkzeug für Webentwickler. Sie ermöglichen es uns, die Kommunikation zwischen dem Server und dem Client zu steuern. Hier haben wir das klassische Beispiel der Zwischenspeicherung von Inhalten gesehen, aber es gibt noch viele weitere Anwendungsfälle.
Wenn wir zum Beispiel die Antworten des Servers zwischenspeichern und zurücksenden, kann der Benutzer unsere Anwendung auch offline weiter nutzen.
Dieses Caching-Beispiel zeigt unsere Lösung für unser Problem - und sie löst es tatsächlich. Aber Service Worker erfüllen viele andere Bedürfnisse in der Entwicklung. Ich würde mich freuen, Ihre zu hören 🙂 Lassen Sie mich wissen, was Sie mit Service Workers bauen auf Twitter oder auf dem Vonage Community Slack!
Vielen Dank an Oria Biton und Miki Ezra Stanger für die freundliche und gründliche Durchsicht dieses Artikels
Teilen Sie:
Yonatan war an einigen großartigen Projekten in der Akademie und in der Industrie beteiligt - von C/C++ über Matlab bis hin zu PHP und Javascript. Früher war er CTO bei Webiks und Softwarearchitekt bei WalkMe. Derzeit ist er Softwarearchitekt bei Vonage und egghead-Dozent.