https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-javascript-service-workers-increased-our-site-speed-by-97-5/js_service-workers.png

Comment les travailleurs de service Javascript ont augmenté la vitesse de notre site de 97,5%.

Publié le August 10, 2023

Temps de lecture : 8 minutes

Voici comment nous avons accéléré le chargement de notre site web de 97,5 % en utilisant des travailleurs de service, comment nous nous assurons que les utilisateurs obtiennent la version la plus récente à chaque fois, et comment vous pouvez le faire aussi.

Le site web du système de conception de Vivid a beaucoup évolué en l'espace d'un an. Il a commencé par quelques pages de base et s'est transformé en un site de documentation à part entière, présentant des exemples concrets d'utilisation de notre produit.

Nous utilisons largement la division de code dans notre code, et notre produit inclut diverses ressources statiques telles que des icônes et des fichiers CSS. Pour couronner le tout, notre documentation utilise des iFrames pour présenter nos composants, chacun appelant ses dépendances séparément (oui, comme une architecture Microfrontend).

Mais au fil du temps, les choses ont commencé à se gâter.

Cela a commencé avec notre environnement de développement. Après quelques rechargements, il se bloquait.

Vivid Docs Loading Errorvivid-docs-loading-error.gif

Nous pouvions nous en accommoder. Ce que nous ne pouvions pas accepter, c'est que nos utilisateurs nous envoient un ticket pour nous montrer à quel point la situation était grave.

Chrome Performance User Ticketvivid-user-ticket.png

Nous savions que nous devions faire quelque chose. Nous devions accélérer notre site web. Voici comment nous avons débogué et corrigé les performances de notre site web.

L'augmentation des performances

Commençons par la fin. Notre objectif principal était de réduire le nombre d'appels au serveur. Un objectif secondaire (ou bonus, si vous voulez) était d'accélérer le temps de chargement des pages de notre application. Vous pouvez voir les résultats et juger par vous-même :

Avant

Vivid Performance Before Service Workersvivid-performance-before.png

Dans l'image ci-dessus, nous pouvons voir le profil des demandes de réseau avant le changement. À certains moments, le chargement d'une page prenait 860 ms, ce qui est énorme ! Le temps de chargement moyen d'une page était d'environ 400 ms. Dans la section la plus à droite, vous pouvez voir la barre grise du site essayant de charger le fichier all.css qui s'éternise. Le site s'arrête alors de se charger et si l'utilisateur essaie d'actualiser la page, cela ne fait qu'empirer les choses !

Vivid Performance Before Service Workersvivid-performance-after.png

Alors, comment avons-nous fait ?

Problème : La limite de connexion de Chrome

Chrome propose une limite de 6 connexions simultanées à un serveur. Ce qui nous a surpris, c'est le long délai d'attente et le fait que les demandes restent "en direct" même lorsque l'on rafraîchit la page ou que l'on navigue sur une autre page.

Comme nous avions près d'une centaine de requêtes sur chaque page (nous fractionnons le code à l'extrême), Chrome coupait la connexion à notre site web pour chaque utilisateur après seulement quelques minutes de navigation. Nous nous sommes donc retrouvés avec une mission : réduire le nombre de fichiers que Chrome devait charger.

Problème : trop de fichiers

Tentative n° 1 : Supprimer Prefetch

Comment étions-nous sûrs d'avoir trop de fichiers ? Lorsque nous avons ouvert l'inspecteur de chrome, notre code ressemblait à ceci :

Chrome Inspector Loading Too Many Fileschrome-inspector-before-service-workers.png

Ces fichiers ne sont qu'une petite partie de notre manifeste des fichiers. Voyez que nous avons également préfixé les fichiers pour accélérer le temps de chargement des pages suivantes.

Notre première mesure a été d'arrêter la prélecture. Bien que cela ait réduit le nombre de requêtes, cela n'a pas aidé parce que le prefetch ne se produisait que lorsque le réseau était inactif, donc il n'y avait pas beaucoup d'effet. Nous devions donc supprimer le nombre de scripts demandés.

Tentative numéro 2 : Regrouper tous les fichiers en une seule grosse liasse

Dans notre projet, nous utilisons rollup pour regrouper nos fichiers. Notre configuration est destinée à tout découper en code et à laisser les consommateurs utiliser leurs propres bundlers pour découper en code, regrouper et agiter l'arbre.

Dans cette étape, nous avons juste passé en revue tous les composants, créé un fichier barrel, et les avons tous regroupés dans un gros fichier `vivid-components.js` que nous avons utilisé à la place de toutes les autres balises de script. Regardez ce commit pour voir comment nous avons procédé.

Cela a aidé sur la plupart des pages mais - vous souvenez-vous des iFrames ? Elles apportaient encore beaucoup de choses - des doublons des fichiers déjà apportés par la page d'accueil.

C'est ainsi que nous en sommes arrivés à notre solution finale et la plus efficace : les travailleurs de service.

Solution : Travailleurs sociaux

Un service worker est une couche entre notre application et le réseau. Il peut écouter toutes les requêtes entrant et sortant de l'application et les traiter.

Dans notre cas, nous voulions traiter les demandes, mettre la réponse en cache et renvoyer la réponse aux demandes suivantes.

Comment enregistrer un Service Worker

La première étape consiste à enregistrer le travailleur de service dans le client :

(async function() {
	const registration = await navigator.serviceWorker.register(
		'/sw.js',
		{
			scope: '/',
		}
	);
})();

Le service worker peut récupérer les requêtes en fonction du dossier qu'il contient. C'est pourquoi je l'ai placé à la racine de mon projet.

Dans notre projet, le fichier n'est pas à la racine. Il y est déplacé dans notre processus de construction, ce qui nous donne une expérience de développement agréable tout en nous permettant de récupérer des requêtes à partir de la racine. Vous pouvez utiliser l'option service-worker-allowed http-header pour le servir dans un autre dossier, mais en utilisant l'astuce "build to root", nous n'en avions pas besoin.

Fonctionnalité du travailleur de service

Notre agent de service se présente comme suit :

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',
		})
	);
});

Nous disposons de deux fonctions utilitaires : addResourcesToCache ajouter une ressource au cache et putInCache pour placer une requête et sa réponse dans le cache.

Ils utilisent tous deux les caches qui nous donne accès à l'objet global CacheStorage à l'objet CacheStorage.

cacheFirst c'est là que la magie opère. Il tente d'obtenir la réponse du cache. Si elle la trouve, elle renvoie la réponse mise en cache. (Lignes 12-15)

Si ce n'est pas le cas, il essaie d'obtenir la réponse d'un préchargement. Si cela fonctionne, c'est bon, nous le mettons en cache et renvoyons la réponse préchargée. (Lignes 17-22)

En cas d'échec, nous passons à la demande du réseau (par exemple, le serveur), obtenons la réponse et la mettons en cache. (Lignes 24-27)

En cas d'échec, nous renvoyons une erreur accompagnée d'une image. (Lignes 29-35)

Cycle de vie du travailleur de service

Le cycle de vie du travailleur de service :

  1. L'enregistrement (nous sommes passés par là)

  2. Installation

  3. Activation

Notre agent de service écoute la phase d'installation et ajoute nos ressources principales au cache.

self.addEventListener('install', (event) => {
	event.waitUntil(
		addResourcesToCache([
			'./',
			'./index.html',
			'/assets/styles/core/all.css',
			'/assets/scripts/vivid-components.js',
			'/assets/scripts/live-sample.js',
		])
	);
});

Remarquez l'utilité waitUntil que nous obtenons sur l'objet événement. Cet utilitaire nous aide à éviter les conditions de course en attendant que les opérations asynchrones se terminent.

Ensuite, dans la phase activate nous activons le préchargement du contenu (avec waitUntil).

const enableNavigationPreload = async () => {
	if (self.registration.navigationPreload) {
		await self.registration.navigationPreload.enable();
	}
};

self.addEventListener('activate', (event) => {
	event.waitUntil(enableNavigationPreload());
});

La dernière étape consiste à ajouter un écouteur à la fonction fetch. Cet écouteur intercepte les requêtes et nous permet de les traiter à l'aide de notre cacheFirst à l'aide de notre fonction

self.addEventListener('fetch', (event) => {
	event.respondWith(
		cacheFirst({
			request: event.request,
			preloadResponsePromise: event.preloadResponse,
			fallbackUrl: './assets/images/vivid-logo.jpeg',
		})
	);
});

Notez que le respondWith utilitaire. Il fait littéralement ce qu'il dit - étant donné la requête, nous pouvons renvoyer n'importe quelle réponse. Dans ce cas, nous renvoyons le résultat de cacheFirst.

Comment gérer les versions dans un Service Worker ?

Il peut arriver que vous souhaitiez mettre à jour la version d'un service worker. Dans notre cas, c'est nécessaire pour la mise à jour de notre bibliothèque. Pour cela, nous devons indiquer la version dans le fichier de notre Service Worker, créer un cache versionné et supprimer l'ancien cache.

Comment créer un cache versionné dans un Service Worker ?

L'ajout d'une version est simple : const VERSION = ‘3.17.0’;

Il peut être modifié manuellement à chaque version.

Dans notre projet, par exemple, nous utilisons le rollup pour regrouper, nous avons donc fait le "truc" suivant :

  1. Nous avons défini la version de cette manière : const VERSION = ‘SW_VERSION’;

  2. Pendant la construction, nous extrayons la version de notre fichier package.json

  3. Nous utilisons le plugin replace du rollup pour définir la version à chaque build.

Vous pouvez voir notre configuration ici.

Comment créer un cache versionné dans un Service Worker ?

Si vous avez remarqué, la méthode caches.open accepte une chaîne de caractères :

const cache = await caches.open(VERSION);

Il attend un nom ou un identifiant de cache auquel nous pourrons nous référer ultérieurement.

Vous aurez souvent besoin de mettre à jour le service worker ou le cache de votre site web. Par exemple, lorsque vous modifiez la version d'une bibliothèque, que vous ajoutez une nouvelle section à la page et que vous modifiez la réponse, vous voulez que cela s'affiche sans attendre l'expiration du cache.

L'utilisation de la version comme identifiant nous permet d'adresser le cache de chaque version et donc d'afficher la version actuelle et de supprimer les anciennes.

Comment supprimer un cache obsolète dans un Service Worker ?

Maintenant que nous avons un cache versionné, nous pouvons le supprimer.

Créons une fonction 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();
	});
}

La fonction passe en revue toutes les clés du cache (ligne 2), et pour chaque clé qui n'est pas la nouvelle version activée, nous la supprimons (ligne 6). Une fois cela fait, nous utilisons la méthode self.clients.claim pour indiquer au navigateur que notre nouveau Service Worker contrôle désormais tous les onglets (ligne 9).

Nous appelons cette fonction pendant l'activation :

self.addEventListener('activate', (event) => {
	event.waitUntil(removeOldCache(event));
	event.waitUntil(enableNavigationPreload());
});

Notez que le cache est le cache global pour tous les travailleurs de service. Dans notre cas, nous pouvons les supprimer en toute sécurité, mais dans votre cas, vous pouvez avoir plus d'un cache et donc envisager d'utiliser un suffixe à la version pour ne supprimer que le cache que vous voulez.

Par exemple, vous souhaiteriez séparer les caches HTML et les caches des requêtes du serveur. Le cache HTML s'effacera lorsque vous modifierez les zones liées au client, tandis que le cache des requêtes du serveur variera en fonction d'autres conditions.

Mise à jour des travailleurs de service

Les nouveaux agents de service sont soumis à une période d'attente. Leur remplacement peut prendre quelques heures. Nous pouvons sauter cette période d'attente en ajoutant self.skipWaiting() dans notre phase d'installation.

Désormais, notre Service Worker sera mis à jour immédiatement lorsque nous le modifierons (par exemple, en téléchargeant une nouvelle version de la bibliothèque).

Résumé

Les Service Workers sont un outil très puissant pour les développeurs web. Ils nous permettent de contrôler la communication entre le serveur et le client. Nous avons vu ici l'exemple classique de la mise en cache de contenu, mais il existe de nombreux autres cas d'utilisation.

Par exemple, si nous mettons en cache les réponses du serveur et les renvoyons, l'utilisateur peut continuer à utiliser notre application tout en étant hors ligne.

Cet exemple de mise en cache montre notre solution à notre problème - et elle le résout en effet. Mais les Service Workers répondent à de nombreux autres besoins en matière de développement. Je serais heureux de connaître les vôtres 🙂 Faites-moi savoir ce que vous construisez avec les Service Workers sur Twitter ou sur le Communauté Vonage Slack!

Un grand merci à Oria Biton et à Miki Ezra Stanger pour la relecture aimable et approfondie de cet article

Partager:

https://a.storyblok.com/f/270183/400x400/7bf76cb05c/yonatankra.png
Yonatan KraArchitecte logiciel Vonage

Yonatan a participé à des projets impressionnants à l'université et dans l'industrie - de C/C++ à PHP et javascript en passant par Matlab. Il a été directeur technique chez Webiks et architecte logiciel chez WalkMe. Il est actuellement architecte logiciel chez Vonage et instructeur.