L'histoire de ce qui a été publié, comment l'impact a été mesuré et les compromis qui ont été faits.
Contexte
Recherchez n'importe quel sujet sur Google, et une page de résultats pertinents et significatifs s'affiche immédiatement. Ce que vous n'avez probablement pas réalisé, c'est que cette page de résultats de recherche est, dans certains cas, gérée par une technologie Web puissante appelée service worker.
Le déploiement de la compatibilité avec les service workers pour la recherche Google sans nuire aux performances a nécessité des dizaines d'ingénieurs travaillant dans plusieurs équipes. Voici l'histoire de ce qui a été publié, de la façon dont les performances ont été mesurées et des compromis effectués.
Principales raisons d'explorer les service workers
L'ajout d'un service worker à une application Web, tout comme toute modification architecturale de votre site, doit être effectué en gardant à l'esprit un ensemble d'objectifs clairs. Pour l'équipe Google Search, l'ajout d'un service worker était intéressant pour plusieurs raisons.
Mise en cache limitée des résultats de recherche
L'équipe de la recherche Google a constaté que les utilisateurs recherchent souvent les mêmes termes plusieurs fois dans un court laps de temps. Plutôt que de déclencher une nouvelle requête backend juste pour obtenir les mêmes résultats, l'équipe Search souhaitait tirer parti du cache et traiter ces requêtes répétées localement.
L'importance de la fraîcheur ne peut pas être négligée. Parfois, les utilisateurs recherchent les mêmes termes à plusieurs reprises, car il s'agit d'un sujet en constante évolution et ils s'attendent à voir des résultats récents. L'utilisation d'un service worker permet à l'équipe chargée de la recherche d'implémenter une logique précise pour contrôler la durée de vie des résultats de recherche mis en cache localement et d'obtenir l'équilibre exact entre la rapidité et la fraîcheur qui, selon elle, répond le mieux aux besoins des utilisateurs.
Expérience hors connexion enrichissante
De plus, l'équipe chargée de la recherche Google souhaitait proposer une expérience hors connexion pertinente. Lorsqu'un utilisateur souhaite en savoir plus sur un sujet, il veut accéder directement à la page de recherche Google et commencer à chercher, sans se soucier d'une connexion Internet active.
Sans service worker, l'accès à la page de recherche Google en mode hors connexion ne mènerait qu'à la page d'erreur réseau standard du navigateur, et les utilisateurs devraient se souvenir de revenir et réessayer une fois leur connexion rétablie. Avec un service worker, vous pouvez diffuser une réponse HTML hors connexion personnalisée et autoriser les utilisateurs à saisir immédiatement leur requête de recherche.
Les résultats ne seront disponibles qu'une fois qu'une connexion Internet sera établie, mais le service worker permet de différer la recherche et de l'envoyer aux serveurs Google dès que l'appareil se reconnecte à l'aide de l'API de synchronisation en arrière-plan.
Mise en cache et diffusion JavaScript plus intelligentes
Nous avons également voulu optimiser le mise en cache et le chargement du code JavaScript modulaire qui alimente les différents types de fonctionnalités sur la page des résultats de recherche. Le regroupement JavaScript offre un certain nombre d'avantages qui ont du sens lorsqu'aucun service worker n'est impliqué. L'équipe Search ne voulait donc pas simplement arrêter complètement le regroupement.
En utilisant la capacité d'un service worker à mettre en version et à mettre en cache des segments de code JavaScript précis au moment de l'exécution, l'équipe Search a pensé pouvoir réduire la quantité de modifications du cache et s'assurer que le code JavaScript réutilisé à l'avenir pouvait être mis en cache de manière efficace. La logique de son service worker peut analyser une requête HTTP sortante pour un bundle contenant plusieurs modules JavaScript, et l'exécuter en assemblant plusieurs modules mis en cache localement (en "dégroupant" efficacement le bundle lorsque cela est possible). Cela économise la bande passante de l'utilisateur et améliore la réactivité globale.
L'utilisation de JavaScript mis en cache par un service worker présente également des avantages en termes de performances: dans Chrome, une représentation au format octets analysée de ce code JavaScript est stockée et réutilisée, ce qui réduit la quantité de travail à effectuer au moment de l'exécution pour exécuter le code JavaScript sur la page.
Défis et solutions
Voici quelques-uns des obstacles à surmonter pour atteindre les objectifs de l'équipe. Bien que certains de ces défis soient spécifiques à la recherche Google, beaucoup d'entre eux s'appliquent à un large éventail de sites qui pourraient envisager de déployer un service worker.
Problème: frais généraux du service worker
Le plus grand défi, et le seul véritable obstacle au lancement d'un service worker dans la recherche Google, était de s'assurer qu'il ne faisait rien qui puisse augmenter la latence perçue par l'utilisateur. La recherche Google prend les performances très au sérieux. Par le passé, elle a bloqué le lancement de nouvelles fonctionnalités si elles entraînaient même quelques dizaines de millisecondes de latence supplémentaire pour une population d'utilisateurs donnée.
Lorsque l'équipe a commencé à collecter des données sur les performances lors de ses premières expériences, il est devenu évident qu'il y aurait un problème. Le code HTML renvoyé en réponse aux requêtes de navigation pour la page de résultats de recherche est dynamique et varie considérablement en fonction de la logique qui doit s'exécuter sur les serveurs Web de la recherche. Il n'existe actuellement aucun moyen pour le service worker de répliquer cette logique et de renvoyer immédiatement le code HTML mis en cache. Le mieux qu'il puisse faire est de transmettre les requêtes de navigation aux serveurs Web backend, ce qui nécessite une requête réseau.
Sans service worker, cette requête réseau se produit immédiatement lors de la navigation de l'utilisateur. Lorsqu'un service worker est enregistré, il doit toujours être démarré et avoir la possibilité d'exécuter ses gestionnaires d'événements fetch
, même s'il est impossible que ces gestionnaires de récupération ne fassent que se connecter au réseau. Le temps nécessaire pour démarrer et exécuter le code du worker de service est un coût supplémentaire ajouté à chaque navigation:
Cela entraîne une latence trop importante pour l'implémentation du service worker pour justifier d'autres avantages. De plus, l'équipe a constaté que, d'après la mesure des temps de démarrage du service worker sur des appareils réels, la distribution des temps de démarrage était large, certains appareils mobiles bas de gamme prenant presque autant de temps à démarrer le service worker que pour effectuer la requête réseau pour le code HTML de la page de résultats.
Solution: utiliser le préchargement de la navigation
La fonctionnalité la plus importante qui a permis à l'équipe de la recherche Google de lancer son service worker est le préchargement de navigation. L'utilisation du préchargement de navigation est un avantage de performance clé pour tout service worker qui doit utiliser une réponse du réseau pour répondre aux requêtes de navigation. Il fournit un indice au navigateur pour qu'il commence à envoyer la requête de navigation immédiatement, au même moment que le service worker démarre:
Tant que le temps de démarrage du service worker est inférieur au temps nécessaire pour obtenir une réponse du réseau, il ne devrait pas y avoir de surcharge de latence introduite par le service worker.
L'équipe Search devait également éviter d'utiliser un service worker sur des appareils mobiles bas de gamme, où le temps de démarrage du service worker pouvait dépasser la requête de navigation. Étant donné qu'il n'existe pas de règle stricte pour déterminer ce qui constitue un appareil "bas de gamme", ils ont mis au point l'heuristique consistant à vérifier la quantité totale de RAM installée sur l'appareil. Tout appareil disposant de moins de 2 Go de mémoire était classé dans la catégorie des appareils bas de gamme, pour lesquels le temps de démarrage du service worker serait inacceptable.
L'espace de stockage disponible est une autre considération, car l'ensemble complet des ressources à mettre en cache pour une utilisation future peut atteindre plusieurs mégaoctets. L'interface navigator.storage
permet à la page de recherche Google de déterminer à l'avance si ses tentatives de mise en cache de données risquent d'échouer en raison d'échecs de quota de stockage.
L'équipe chargée de la recherche disposait ainsi de plusieurs critères qu'elle pouvait utiliser pour déterminer si elle devait utiliser ou non un service worker: si un utilisateur accède à la page de recherche Google à l'aide d'un navigateur compatible avec le préchargement de navigation, et qu'il dispose d'au moins 2 Go de RAM et d'espace de stockage libre suffisant, un service worker est enregistré. Les navigateurs ou appareils qui ne répondent pas à ces critères ne bénéficieront pas d'un service worker, mais ils auront toujours la même expérience de recherche Google qu'auparavant.
L'enregistrement sélectif présente un avantage indirect : vous pouvez déployer un service worker plus petit et plus efficace. Cibler des navigateurs assez récents pour exécuter le code du service worker élimine les frais liés à la transpilation et aux polyfills pour les anciens navigateurs. Cela a permis de supprimer environ 8 ko de code JavaScript non compressé de la taille totale de l'implémentation du service worker.
Problème: niveaux d'accès des service workers
Une fois que l'équipe Search a effectué suffisamment d'expériences de latence et qu'elle a été convaincue que l'utilisation du préchargement de navigation leur offrait un chemin viable et neutre en termes de latence pour utiliser un service worker, certains problèmes pratiques ont commencé à prendre le devant de la scène. L'un de ces problèmes concerne les règles de champ d'application du service worker. La portée d'un service worker détermine les pages qu'il peut potentiellement contrôler.
Le champ d'application fonctionne en fonction du préfixe du chemin d'URL. Pour les domaines qui hébergent une seule application Web, ce n'est pas un problème, car vous n'utilisez normalement qu'un seul service worker avec la portée maximale de /
, qui peut prendre le contrôle de n'importe quelle page du domaine.
Toutefois, la structure d'URL de la recherche Google est un peu plus complexe.
Si le service worker recevait la portée maximale de /
, il pourrait prendre le contrôle de n'importe quelle page hébergée sous www.google.com
(ou l'équivalent régional), et il existe des URL sous ce domaine qui n'ont rien à voir avec la recherche Google. Une portée plus raisonnable et restrictive serait /search
, qui permettrait au moins d'éliminer les URL complètement sans rapport avec les résultats de recherche.
Malheureusement, même ce chemin d'URL /search
est partagé entre les différents types de résultats de recherche Google, les paramètres de requête de l'URL déterminant le type spécifique de résultat de recherche affiché. Certaines de ces variantes utilisent des bases de code complètement différentes de la page de résultats de recherche Web traditionnelle. Par exemple, la recherche d'images et la recherche Shopping sont toutes deux diffusées sous le chemin d'URL /search
avec des paramètres de requête différents, mais aucune de ces interfaces n'était prête à proposer sa propre expérience de service worker (pour le moment).
Solution: créer un framework de distribution et de routage
Bien que certaines propositions permettent d'utiliser des éléments plus puissants que les préfixes de chemin d'URL pour déterminer les champs d'application des service workers, l'équipe Google Search était bloquée dans le déploiement d'un service worker qui ne faisait rien pour un sous-ensemble de pages qu'elle contrôlait.
Pour contourner ce problème, l'équipe Google Search a développé un framework de distribution et d'acheminement personnalisé pouvant être configuré pour vérifier des critères tels que les paramètres de requête de la page client, et les utiliser pour déterminer le chemin de code spécifique à suivre. Plutôt que de coder en dur des règles, le système a été conçu pour être flexible et permettre aux équipes qui partagent l'espace d'URL, comme la recherche d'images et la recherche Shopping, d'intégrer leur propre logique de service worker à terme, si elles décident de l'implémenter.
Problème: résultats et métriques personnalisés
Les utilisateurs peuvent se connecter à la recherche Google avec leur compte Google. Leur expérience avec les résultats de recherche peut être personnalisée en fonction des données de leur compte. Les utilisateurs connectés sont identifiés par des cookies de navigateur spécifiques, une norme ancienne et largement acceptée.
L'inconvénient de l'utilisation de cookies de navigateur est qu'ils ne sont pas exposés dans un service worker. Il n'existe aucun moyen d'examiner automatiquement leurs valeurs et de s'assurer qu'elles n'ont pas changé en raison de la déconnexion ou du changement de compte d'un utilisateur. (Des efforts sont en cours pour permettre aux services workers d'accéder aux cookies, mais à l'heure actuelle, cette approche est expérimentale et n'est pas largement compatible.)
Une incohérence entre la vue de l'utilisateur actuellement connecté par le service worker et l'utilisateur réel connecté à l'interface Web de la recherche Google peut entraîner des résultats de recherche personnalisés de manière incorrecte, ou des métriques et des journaux attribués de manière incorrecte. Chacun de ces scénarios d'échec constituerait un problème grave pour l'équipe chargée de la recherche Google.
Solution: envoyer des cookies à l'aide de postMessage
Plutôt que d'attendre le lancement des API expérimentales et de fournir un accès direct aux cookies du navigateur dans un service worker, l'équipe Google Search a opté pour une solution provisoire: chaque fois qu'une page contrôlée par le service worker est chargée, elle lit les cookies pertinents et utilise postMessage()
pour les envoyer au service worker.
Le service worker vérifie ensuite la valeur actuelle du cookie par rapport à la valeur attendue. En cas de non-correspondance, il prend des mesures pour supprimer toutes les données spécifiques à l'utilisateur de son stockage et recharge la page de résultats de recherche sans personnalisation incorrecte.
Les étapes spécifiques que le service worker effectue pour rétablir une référence sont propres aux exigences de la recherche Google, mais la même approche générale peut être utile à d'autres développeurs qui traitent des données personnalisées basées sur les cookies des navigateurs.
Problème: tests et dynamisme
Comme indiqué, l'équipe de la recherche Google s'appuie fortement sur l'exécution de tests en production et sur le test des effets du nouveau code et des nouvelles fonctionnalités dans le monde réel avant de les activer par défaut. Cela peut être un peu difficile avec un service worker statique qui s'appuie fortement sur les données mises en cache, car l'activation et la désactivation des tests nécessitent souvent une communication avec le serveur backend.
Solution: script de service worker généré dynamiquement
La solution choisie par l'équipe a consisté à utiliser un script de service worker généré dynamiquement, personnalisé par le serveur Web pour chaque utilisateur, au lieu d'un script de service worker statique unique généré à l'avance. Les informations sur les tests susceptibles d'affecter le comportement du service worker ou les requêtes réseau en général sont incluses directement dans ces scripts de service worker personnalisés. La modification des ensembles d'expériences actives pour un utilisateur s'effectue via une combinaison de techniques traditionnelles, comme les cookies du navigateur, et de l'envoi de code mis à jour dans l'URL du service worker enregistré.
L'utilisation d'un script de service worker généré dynamiquement permet également de fournir plus facilement une issue de secours dans le cas peu probable qu'une implémentation de service worker présente un bug fatal à éviter. La réponse du service worker de serveur dynamique peut être une implémentation sans opération, ce qui désactive efficacement le service worker pour certains ou tous les utilisateurs actuels.
Problème: coordination des mises à jour
L'un des défis les plus difficiles à relever lors du déploiement d'un service worker dans le monde réel consiste à trouver un compromis raisonnable entre éviter le réseau au profit du cache, tout en veillant à ce que les utilisateurs existants reçoivent les mises à jour et les modifications critiques peu de temps après leur déploiement en production. Le bon équilibre dépend de nombreux facteurs:
- Indique si votre application Web est une application monopage durable qu'un utilisateur laisse ouverte indéfiniment, sans accéder à de nouvelles pages.
- La fréquence de déploiement des mises à jour de votre serveur Web backend.
- Si l'utilisateur moyen tolère d'utiliser une version légèrement obsolète de votre application Web ou si la fraîcheur est la priorité absolue.
Lors des tests des services workers, l'équipe Google Search s'est assurée de maintenir les tests sur un certain nombre de mises à jour de backend planifiées afin de s'assurer que les métriques et l'expérience utilisateur correspondraient plus étroitement à ce que les utilisateurs réguliers verraient dans le monde réel.
Solution: Équilibrer la fraîcheur et l'utilisation du cache
Après avoir testé un certain nombre d'options de configuration différentes, l'équipe Google Search a constaté que la configuration suivante offrait le juste équilibre entre la fraîcheur et l'utilisation du cache.
L'URL du script du service worker est diffusée avec l'en-tête de réponse Cache-Control: private, max-age=1500
(1 500 secondes ou 25 minutes) et est enregistrée avec updateViaCache défini sur "all" pour s'assurer que l'en-tête est respecté. Comme vous pouvez l'imaginer, le backend Web de la recherche Google est un ensemble de serveurs volumineux et distribués dans le monde entier qui nécessite une disponibilité proche de 100 %. Le déploiement d'une modification qui affecterait le contenu du script du service worker est effectué de manière progressive.
Si un utilisateur accède à un backend qui a été mis à jour, puis accède rapidement à une autre page qui accède à un backend qui n'a pas encore reçu le worker de service mis à jour, il finira par passer plusieurs fois d'une version à l'autre. Par conséquent, demander au navigateur de ne vérifier la présence d'un script mis à jour que si 25 minutes se sont écoulées depuis la dernière vérification n'a pas d'inconvénient majeur. L'avantage d'activer ce comportement est de réduire considérablement le trafic reçu par le point de terminaison qui génère dynamiquement le script du service worker.
De plus, un en-tête ETag est défini sur la réponse HTTP du script du service worker, ce qui garantit qu'une fois que la vérification des mises à jour est effectuée après 25 minutes, le serveur peut répondre efficacement avec une réponse HTTP 304 si aucune mise à jour du service worker n'a été déployée entre-temps.
Bien que certaines interactions dans l'application Web de la recherche Google utilisent des navigations de type application à page unique (par exemple, via l'API History), la recherche Google est dans la plupart des cas une application Web traditionnelle qui utilise des navigations "réelles". Cela entre en jeu lorsque l'équipe a décidé qu'il serait efficace d'utiliser deux options qui accélèrent le cycle de vie de mise à jour du service worker : clients.claim()
et skipWaiting()
.
Cliquer dans l'interface de la recherche Google vous permet généralement d'accéder à de nouveaux documents HTML. L'appel de skipWaiting
garantit qu'un service worker mis à jour a la possibilité de gérer ces nouvelles requêtes de navigation immédiatement après l'installation. De même, appeler clients.claim()
signifie que le service worker mis à jour peut commencer à contrôler toutes les pages Google Search ouvertes qui ne sont pas contrôlées, après l'activation du service worker.
L'approche adoptée par la recherche Google n'est pas nécessairement une solution qui fonctionne pour tous. Elle est le résultat de tests A/B minutieux sur différentes combinaisons d'options de diffusion jusqu'à ce que l'équipe trouve la solution la plus adaptée.
Les développeurs dont l'infrastructure backend leur permet de déployer des mises à jour plus rapidement peuvent préférer que le navigateur recherche un script de service worker mis à jour aussi souvent que possible, en toujours ignorant le cache HTTP.
Si vous créez une application monopage que les utilisateurs peuvent laisser ouverte pendant une longue période, l'utilisation de skipWaiting()
n'est probablement pas le bon choix. Vous risquez de rencontrer des incohérences de cache si vous autorisez le nouveau service worker à s'activer en présence de clients de longue durée.
Points clés à retenir
Par défaut, les service workers ne sont pas neutres en termes de performances
Ajouter un service worker à votre application Web signifie insérer un élément JavaScript supplémentaire qui doit être chargé et exécuté avant que votre application Web ne reçoive des réponses à ses requêtes. Si ces réponses proviennent finalement d'un cache local plutôt que du réseau, les coûts liés à l'exécution du service worker sont généralement négligeables par rapport à l'amélioration des performances obtenue en privilégiant le cache. Toutefois, si vous savez que votre service worker doit toujours consulter le réseau lors du traitement des requêtes de navigation, l'utilisation du préchargement de navigation est un gain de performances crucial.
Les services workers sont (toujours) une amélioration progressive
La situation est bien meilleure aujourd'hui qu'il y a un an. Tous les navigateurs modernes sont désormais compatibles, au moins partiellement, avec les service workers. Malheureusement, certaines fonctionnalités avancées de service worker (telles que la synchronisation en arrière-plan et le préchargement de navigation) ne sont pas déployées universellement. Vérifier les fonctionnalités pour le sous-ensemble spécifique de fonctionnalités dont vous savez que vous avez besoin et n'enregistrer un service worker que lorsqu'elles sont présentes reste une approche raisonnable.
De même, si vous avez effectué des tests sur le terrain et que vous savez que les appareils bas de gamme finissent par fonctionner mal avec les frais généraux supplémentaires d'un service worker, vous pouvez également vous abstenir d'enregistrer un service worker dans ces scénarios.
Vous devez continuer à traiter les service workers comme une amélioration progressive qui est ajoutée à une application Web lorsque toutes les conditions préalables sont remplies et que le service worker apporte quelque chose de positif à l'expérience utilisateur et aux performances de chargement globales.
Tout mesurer
Le seul moyen de savoir si l'envoi d'un service worker a eu un impact positif ou négatif sur l'expérience de vos utilisateurs est de tester et de mesurer les résultats.
Les détails de la configuration de mesures pertinentes dépendent du fournisseur d'analyse que vous utilisez et de la manière dont vous effectuez normalement des tests dans votre configuration de déploiement. Une approche, qui consiste à utiliser Google Analytics pour collecter des métriques, est détaillée dans cette étude de cas basée sur l'expérience d'utilisation des service workers dans l'application Web Google I/O.
Non-objectifs
Bien que de nombreux membres de la communauté du développement Web associent les service workers aux progressive web apps, l'objectif initial de l'équipe n'était pas de créer une "PWA Google Search". L'application Web de la recherche Google ne fournit actuellement pas de métadonnées via un fichier manifeste d'application Web, ni n'encourage les utilisateurs à suivre la procédure d'ajout à l'écran d'accueil. L'équipe chargée de la recherche est actuellement satisfaite des utilisateurs qui accèdent à son application Web via les points d'entrée traditionnels de la recherche Google.
Plutôt que d'essayer de transformer l'expérience Web de la recherche Google en équivalent de ce que vous attendez d'une application installée, l'objectif du déploiement initial était d'améliorer progressivement le site Web existant.
Remerciements
Merci à toute l'équipe de développement Web de la recherche Google pour son travail sur l'implémentation du service worker et pour avoir partagé les informations de référence qui ont servi à rédiger cet article. Merci tout particulièrement à Philippe Golle, Rajesh Jagannathan et R. Samuel Klatchko, Andy Martone, Leonardo Peña, Rachel Shearer, Greg Terrono et Clay Woolam.
Mise à jour (octobre 2021): depuis la publication initiale de cet article, l'équipe Google Search a réévalué les avantages et les inconvénients de son architecture de service worker actuelle. Le service worker décrit ci-dessus va être abandonné. À mesure que l'infrastructure Web de la recherche Google évolue, l'équipe peut revoir la conception de son service worker.