Intégrer les service workers dans la recherche Google

L'histoire de ce qui a été livré, de la façon dont l'impact a été mesuré et des compromis qui ont été faits.

Publié le 20 juin 2019

Recherchez à peu près n'importe quel sujet sur Google, et vous obtiendrez une page de résultats pertinents et utiles, reconnaissable entre mille. Ce que vous ne savez peut-être pas, c'est que cette page de résultats de recherche est, dans certains cas, fournie par une puissante technologie Web appelée service worker.

Le déploiement de la prise en charge des service workers pour la recherche Google sans impact négatif sur les performances a nécessité le travail de dizaines d'ingénieurs répartis dans plusieurs équipes. Voici l'histoire de ce qui a été déployé, de la façon dont les performances ont été mesurées et des compromis qui ont été faits.

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é avec un ensemble d'objectifs clairs en tête. Pour l'équipe de la recherche Google, plusieurs raisons principales justifiaient l'ajout d'un service worker.

Mise en cache limitée des résultats de recherche

L'équipe de la recherche Google a constaté qu'il est courant que les utilisateurs recherchent les mêmes termes plusieurs fois sur une courte période. Plutôt que de déclencher une nouvelle requête backend juste pour obtenir des résultats probablement identiques, l'équipe de recherche a souhaité tirer parti de la mise en cache et répondre à ces requêtes répétées localement.

Il est important de ne pas sous-estimer la fraîcheur des résultats. 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 de 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 rapidité et fraîcheur qui, selon elle, sert le mieux les utilisateurs.

Expérience hors connexion pertinente

L'équipe Recherche Google souhaitait également offrir 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 à effectuer des recherches, sans se soucier d'une connexion Internet active.

Sans service worker, l'accès à la page de recherche Google en mode hors connexion entraînerait simplement l'affichage de la page d'erreur réseau standard du navigateur. Les utilisateurs devraient alors se souvenir de revenir et de réessayer une fois leur connexion rétablie. Avec un service worker, il est possible de fournir une réponse HTML hors connexion personnalisée et de permettre aux utilisateurs de saisir immédiatement leur requête de recherche.

Capture d'écran de l'interface de nouvelle tentative en arrière-plan.

Les résultats ne seront disponibles qu'une fois la connexion Internet rétablie, mais le service worker permet de différer la recherche et de l'envoyer aux serveurs de Google dès que l'appareil est de nouveau en ligne à l'aide de l'API Background Sync.

Mise en cache et diffusion plus intelligentes de JavaScript

Une autre motivation était d'optimiser la mise en cache et le chargement du code JavaScript modularisé qui alimente les différents types de fonctionnalités sur la page des résultats de recherche. Le regroupement JavaScript offre de nombreux avantages qui ont du sens lorsqu'aucun service worker n'est impliqué. L'équipe de recherche n'a donc pas souhaité arrêter complètement le regroupement.

L'équipe de recherche a pensé qu'elle pourrait réduire le taux de renouvellement du cache et s'assurer que le code JavaScript réutilisé à l'avenir puisse être mis en cache efficacement, en utilisant la capacité d'un service worker à versionner et à mettre en cache des blocs de code JavaScript précis au moment de l'exécution. La logique à l'intérieur de leur service worker peut analyser une requête HTTP sortante pour un bundle contenant plusieurs modules JavaScript et la traiter en assemblant plusieurs modules mis en cache localement, ce qui revient à "dégroupage" lorsque cela est possible. Cela permet d'économiser la bande passante de l'utilisateur et d'améliorer la réactivité globale.

L'utilisation de code JavaScript mis en cache et diffusé par un service worker présente également des avantages en termes de performances. Dans Chrome, une représentation analysée en code octet 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 qu'il a fallu surmonter pour atteindre les objectifs fixés par l'équipe. Si certains de ces défis sont spécifiques à la recherche Google, beaucoup d'entre eux s'appliquent à un large éventail de sites qui envisagent de déployer un service worker.

Problème : surcharge du service worker

Le principal défi, et le seul véritable obstacle au lancement d'un service worker sur la recherche Google, était de s'assurer qu'il ne fasse 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 premiers tests, il est devenu évident qu'un problème allait se poser. 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. Le service worker ne peut actuellement pas répliquer cette logique et 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 de 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 lorsqu'il n'y a aucune chance que ces gestionnaires de récupération fassent autre chose que d'accéder au réseau. Le temps nécessaire au démarrage et à l'exécution du code du service worker est une pure surcharge ajoutée à chaque navigation :

Illustration du blocage de la requête de navigation par le service worker au démarrage.

L'implémentation du service worker présente une latence trop importante pour justifier d'autres avantages. L'équipe a également constaté, en mesurant les temps de démarrage des service workers sur des appareils réels, que les temps de démarrage étaient très variables. Sur certains appareils mobiles bas de gamme, le démarrage du service worker prenait presque autant de temps que la requête réseau pour le code HTML de la page de résultats.

Solution : utiliser la précharge de navigation

La fonctionnalité unique et la plus cruciale qui a permis à l'équipe de recherche Google de lancer son service worker est la précharge de navigation. L'utilisation du préchargement de navigation est un avantage clé en termes de performances pour tout service worker qui doit utiliser une réponse du réseau pour répondre aux demandes de navigation. Il fournit au navigateur un indice pour qu'il commence immédiatement à envoyer la requête de navigation, en même temps que le service worker démarre :

Illustration du démarrage du logiciel en parallèle de la requête de navigation.

Tant que le temps nécessaire au 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 chargée de la recherche devait également éviter d'utiliser un service worker sur les appareils mobiles bas de gamme, où le temps de démarrage du service worker pouvait dépasser la requête de navigation. Comme il n'existe pas de règle stricte pour définir un appareil "bas de gamme", ils ont mis au point l'heuristique de vérification de la RAM totale installée sur l'appareil. Tout ce qui était inférieur à 2 gigaoctets de mémoire entrait dans la catégorie des appareils bas de gamme, où le temps de démarrage du service worker serait inacceptable.

L'espace de stockage disponible est un autre élément à prendre en compte, car l'ensemble des ressources à mettre en cache pour une utilisation ultérieure 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 des données risquent d'échouer en raison de problèmes de quota de stockage.

L'équipe de recherche disposait ainsi de plusieurs critères pour déterminer s'il fallait ou non utiliser un service worker : si un utilisateur accède à la page de recherche Google à l'aide d'un navigateur compatible avec la précharge de navigation, et qu'il dispose d'au moins 2 Go de RAM et de suffisamment d'espace de stockage libre, un service worker est enregistré. Les navigateurs ou appareils qui ne répondent pas à ces critères ne bénéficieront pas de service worker, mais ils continueront de profiter de la même expérience de recherche Google que d'habitude.

L'un des avantages de cet enregistrement sélectif est la possibilité d'expédier un service worker plus petit et plus efficace. Cibler des navigateurs relativement récents pour exécuter le code du service worker élimine les frais généraux de transcompilation et de polyfills pour les anciens navigateurs. Cela a permis de supprimer environ 8 kilo-octets de code JavaScript non compressé de la taille totale de l'implémentation du service worker.

Problème : niveaux d'accès du service worker

Une fois que l'équipe de recherche a effectué suffisamment d'expériences de latence et qu'elle était convaincue que l'utilisation du préchargement de navigation lui offrait une voie viable et neutre en termes de latence pour utiliser un service worker, certains problèmes pratiques ont commencé à se manifester. L'un de ces problèmes concerne les règles de portée du service worker. Le champ d'application d'un service worker détermine les pages qu'il peut potentiellement contrôler.

La portée 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'utiliserez normalement qu'un service worker avec la portée maximale de /, qui peut prendre le contrôle de n'importe quelle page du domaine. Toutefois, la structure des URL de la recherche Google est un peu plus complexe.

Si le service worker recevait le champ d'application maximal de /, il finirait par pouvoir 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, ce qui éliminerait au moins les URL qui n'ont absolument rien à voir avec les résultats de recherche.

Malheureusement, même ce chemin d'URL /search est partagé entre différentes versions des résultats de recherche Google, et les paramètres de requête de l'URL déterminent le type spécifique de résultat de recherche affiché. Certaines de ces saveurs utilisent des bases de code complètement différentes de la page de résultats de recherche sur le Web traditionnelle. Par exemple, la recherche d'images et la recherche Shopping sont toutes deux diffusées sous le chemin d'URL /search avec différents paramètres de requête, mais aucune de ces interfaces n'était encore prête à proposer sa propre expérience de service worker.

Solution : créez un framework de répartition et de routage

Bien qu'il existe certaines propositions qui permettent d'utiliser des éléments plus puissants que les préfixes de chemin d'URL pour déterminer les portées 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 Recherche Google a créé un framework de répartition et de routage personnalisé qui pouvait ê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 d'intégrer des règles en dur, 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 à l'avenir, 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 à l'aide de leur compte Google. Leur expérience 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, qui sont une norme éprouvée et largement acceptée.

Toutefois, l'un des inconvénients de l'utilisation des 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 d'une déconnexion ou d'un changement de compte par l'utilisateur. (Des efforts sont en cours pour permettre aux service workers d'accéder aux cookies, mais au moment de la rédaction de cet article, l'approche est expérimentale et n'est pas largement prise en charge.)

Une incohérence entre la vue du service worker concernant l'utilisateur actuellement connecté et l'utilisateur réellement 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 mal attribués. Chacun de ces scénarios d'échec constituerait un problème grave pour l'équipe Google Recherche.

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 de la recherche Google a opté pour une solution provisoire : chaque fois qu'une page contrôlée par le service worker est chargée, la page lit les cookies concernés et utilise postMessage() pour les envoyer au service worker.

Le service worker vérifie ensuite si la valeur actuelle du cookie correspond à celle attendue. En cas de non-concordance, 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 aucune personnalisation incorrecte.

Les étapes spécifiques suivies par le service worker pour rétablir une configuration de base sont propres aux exigences de la recherche Google. Toutefois, 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 du navigateur.

Problème : tests et dynamisme

Comme mentionné précédemment, l'équipe de la recherche Google s'appuie fortement sur les tests en production et sur l'évaluation 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 repose fortement sur les données mises en cache, car l'activation et la désactivation des utilisateurs dans les expériences nécessitent souvent une communication avec le serveur backend.

Solution : script de service worker généré dynamiquement

L'équipe a choisi d'utiliser un script de service worker généré de manière dynamique, personnalisé par le serveur Web pour chaque utilisateur individuel, au lieu d'un script de service worker unique et statique généré à l'avance. Les informations sur les expériences 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. Pour modifier les ensembles d'expériences actives d'un utilisateur, vous devez combiner des techniques traditionnelles, comme les cookies de navigateur, et diffuser du code mis à jour dans l'URL du service worker enregistré.

L'utilisation d'un script de service worker généré de manière dynamique permet également de fournir plus facilement une porte de sortie dans le cas peu probable où une implémentation de service worker présente un bug fatal à éviter. La réponse dynamique du service worker peut être une implémentation no-op, qui désactive effectivement le service worker pour certains ou pour tous les utilisateurs actuels.

Problème : coordonner les mises à jour

L'un des défis les plus difficiles à relever pour tout déploiement de service worker dans le monde réel consiste à trouver un compromis raisonnable entre l'évitement du réseau au profit du cache et la garantie 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 :

  • Que votre application Web soit une application monopage de longue durée qu'un utilisateur garde ouverte indéfiniment, sans naviguer vers de nouvelles pages.
  • Cadence de déploiement des mises à jour de votre serveur Web de backend.
  • Indique si l'utilisateur moyen tolérerait l'utilisation d'une version légèrement obsolète de votre application Web ou si la fraîcheur est la priorité absolue.

Lors de ses tests avec les service workers, l'équipe Google Search s'est assurée de les exécuter sur plusieurs mises à jour planifiées du backend. L'objectif était de s'assurer que les métriques et l'expérience utilisateur correspondaient davantage à 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é différentes options de configuration, l'équipe Google Search a constaté que la configuration suivante offrait le juste équilibre entre fraîcheur et 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, soit 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 grand ensemble de serveurs distribués à l'échelle mondiale, qui nécessite un temps d'activité aussi proche que possible 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 navigue rapidement vers une autre page qui accède à un backend qui n'a pas encore reçu le service worker mis à jour, il risque de passer d'une version à l'autre plusieurs fois. Par conséquent, il n'y a pas d'inconvénient majeur à demander au navigateur de ne vérifier si le script a été mis à jour que si 25 minutes se sont écoulées depuis la dernière vérification. L'avantage de 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 que, lorsqu'une vérification de mise à jour est effectuée après 25 minutes, le serveur peut répondre efficacement avec une réponse HTTP 304 s'il n'y a pas eu de mises à jour du service worker déployé entre-temps.

Bien que certaines interactions dans l'application Web Recherche Google utilisent des navigations de type application à page unique (c'est-à-dire via l'API History), la recherche Google est pour la plupart 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 la mise à jour du service worker : clients.claim() et skipWaiting(). Cliquer sur l'interface de la recherche Google permet généralement d'accéder à de nouveaux documents HTML. L'appel de skipWaiting permet à un service worker mis à jour de gérer ces nouvelles requêtes de navigation immédiatement après l'installation. De même, l'appel de clients.claim() signifie que le service worker mis à jour a la possibilité de commencer à contrôler toutes les pages de recherche Google 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 forcément une solution qui convient à tout le monde. Elle est le résultat de tests A/B minutieux sur différentes combinaisons d'options de diffusion jusqu'à ce que l'équipe trouve celle qui lui convenait le mieux. Les développeurs dont l'infrastructure de 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 ignorant toujours le cache HTTP. Si vous créez une application monopage que les utilisateurs sont susceptibles de laisser ouverte pendant une longue période, l'utilisation de skipWaiting() n'est probablement pas le bon choix pour vous. Vous risquez de rencontrer des incohérences de cache si vous autorisez l'activation du nouveau service worker alors qu'il existe des clients de longue durée.

Points clés à retenir

Par défaut, les service workers n'ont pas un impact neutre sur les 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 reçoive des réponses à ses requêtes. Si ces réponses proviennent d'un cache local plutôt que du réseau, la surcharge liée à l'exécution du service worker est généralement négligeable par rapport au gain de performances obtenu en utilisant la stratégie "cache first". Toutefois, si vous savez que votre service worker doit toujours consulter le réseau lorsqu'il traite les requêtes de navigation, l'utilisation du préchargement de navigation est un gain de performances crucial.

Les service workers sont (toujours) une amélioration progressive

La prise en charge des service workers est bien meilleure aujourd'hui qu'il y a un an. Tous les navigateurs modernes sont désormais au moins partiellement compatibles avec les service workers. Malheureusement, certaines fonctionnalités avancées des service workers, comme la synchronisation en arrière-plan et le préchargement de navigation, ne sont pas déployées de manière universelle. La vérification des fonctionnalités pour le sous-ensemble spécifique de fonctionnalités dont vous savez avoir besoin, et l'enregistrement d'un service worker uniquement lorsque celles-ci sont présentes, reste une approche raisonnable.

De même, si vous avez effectué des tests en conditions réelles et que vous savez que les appareils bas de gamme ont de mauvaises performances avec la surcharge supplémentaire 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 un élément positif à l'expérience utilisateur et aux performances de chargement globales.

Tout mesurer

La seule façon 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 faire des tests et de mesurer les résultats.

Les spécificités de la configuration de mesures pertinentes dépendent du fournisseur d'analyse que vous utilisez et de la façon dont vous effectuez normalement des tests dans votre configuration de déploiement. Une approche utilisant 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.

Ce qui n'est pas un objectif

Bien que de nombreux membres de la communauté des développeurs Web associent les service workers aux progressive web apps, l'équipe n'avait pas pour objectif initial de créer une "PWA Recherche Google". L'application Web Recherche Google ne fournit pas de métadonnées dans un manifeste d'application Web et n'encourage pas les utilisateurs à suivre le flux "Ajouter à l'écran d'accueil". L'équipe chargée de la recherche est satisfaite que les utilisateurs accèdent à son application Web avec les points d'entrée classiques de la recherche Google.

Plutôt que d'essayer de transformer l'expérience Web de la recherche Google en l'équivalent de ce que vous attendez d'une application installée, l'objectif de la version initiale é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 base qui ont permis de rédiger cet article. Nous remercions tout particulièrement Philippe Golle, Rajesh Jagannathan, R. Samuel Klatchko, Andy Martone, Leonardo Peña, Rachel Shearer, Greg Terrono et Clay Woolam.

Mise à jour (octobre 2021) : Depuis la publication de cet article, l'équipe Google Recherche a réévalué les avantages et les inconvénients de son architecture de service worker actuelle. Le service worker décrit ci-dessus est en cours d'arrêt. À mesure que l'infrastructure Web de la recherche Google évolue, l'équipe peut revoir la conception de son service worker.