Intégrer les service workers dans la recherche Google

Histoire de ce qui a été expédié, comment l'impact a été mesuré et les compromis qui ont été faits.

Contexte

Recherchez n'importe quel thème sur Google et accédez à une page de résultats pertinents et reconnaissables, que vous pouvez facilement identifier. Ce que vous n'aviez probablement pas réalisé, c'est que cette page de résultats de recherche était, dans certains cas, diffusée par une technologie Web puissante appelée service worker.

Déploiement de la compatibilité des service workers pour la recherche Google sans impact négatif sur les performances a nécessité des dizaines d'ingénieurs travaillant dans plusieurs équipes. Voici l'histoire de ce qui a été expédié, comment les performances ont été mesurées et les compromis qui ont été faits.

Principales raisons d'explorer les service workers

Comme pour toute modification de l'architecture de votre site, l'ajout d'un service worker à une application Web doit être défini en fonction d'un ensemble précis d'objectifs. L'équipe Google Search a présenté plusieurs raisons clés pour lesquelles l'ajout d'un service worker mérite d'être exploré.

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

L'équipe de 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 de backend dans le seul but d'obtenir des résultats susceptibles d'être les mêmes, l'équipe de recherche souhaitait exploiter la mise en cache et traiter ces requêtes répétées localement.

Il n'est pas possible de dissiper l'importance de l'actualisation, et il arrive que les utilisateurs recherchent les mêmes termes à plusieurs reprises, car il s'agit d'un sujet en constante évolution, et ils s'attendent à obtenir de nouveaux résultats. Le recours à un service worker permet à l'équipe Recherche de mettre en œuvre une logique ultraprécise pour contrôler la durée de vie des résultats de recherche mis en cache localement, et d'obtenir le juste équilibre entre vitesse et fraîcheur, qui, selon elle, répond le mieux aux besoins des utilisateurs.

Expérience hors connexion utile

De plus, l'équipe de recherche Google souhaitait offrir une expérience hors connexion significative. Lorsqu'un utilisateur souhaite en savoir plus sur un sujet, il doit accéder directement à la page de recherche Google et commencer à effectuer une recherche, sans avoir à se soucier d'une connexion Internet active.

Sans service worker, accéder à la page de recherche Google en mode hors connexion renvoyait simplement vers la page d'erreur réseau standard du navigateur. Les utilisateurs devront donc penser à revenir et réessayer une fois la connexion rétablie. Un service worker permet 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 sont disponibles qu'une fois la connexion Internet établie, mais le service worker autorise le report de la recherche et l'envoi 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

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 de résultats de recherche. Le regroupement JavaScript présente de nombreux avantages qui sont pertinents lorsqu'aucun service worker n'est impliqué. L'équipe de recherche ne voulait donc pas simplement l'arrêter complètement.

En se servant de la capacité d'un service worker à gérer les versions et à mettre en cache des fragments précis de JavaScript au moment de l'exécution, l'équipe de recherche a soupçonné la capacité à réduire la perte de mémoire cache et à s'assurer que le code JavaScript réutilisé à l'avenir peut être mis en cache efficacement. La logique de son service worker peut analyser une requête HTTP sortante pour un lot contenant plusieurs modules JavaScript, puis la traiter en assemblant plusieurs modules mis en cache localement, ce qui permet de "dégrouper" si possible. Cela permet d'économiser la bande passante de l'utilisateur et d'améliorer la réactivité globale.

L'utilisation d'un code JavaScript mis en cache diffusé par un service worker présente également des avantages en termes de performances: dans Chrome, une représentation de code JavaScript analysée est stockée et réutilisée, ce qui réduit le nombre de tâches à 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 définis par 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 pouvant envisager le déploiement d'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, consistait à s'assurer qu'il n'effectuait aucun effet susceptible d'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 contribuaient à une latence supplémentaire de plusieurs dizaines de millisecondes 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 serait problématique. 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 devant 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. La meilleure solution consiste à 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 après 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 n'y a aucune chance que ces gestionnaires d'extraction fassent autre chose que se connecter au réseau. Le temps nécessaire au démarrage et à l'exécution du code du service worker correspond à un surcoût pur qui vient s'ajouter à chaque navigation:

Illustration du démarrage du logiciel qui bloque la requête de navigation.

De ce fait, l'implémentation du service worker présente trop d'inconvénients en termes de latence pour justifier tout autre avantage. En outre, l'équipe a constaté qu'en mesurant le temps de démarrage des service workers sur des appareils réels, les temps de démarrage étaient largement répartis. Certains appareils mobiles bas de gamme mettent presque autant de temps à démarrer le service worker qu'à 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é unique et la plus essentielle qui a permis à l'équipe de recherche Google d'avancer dans le lancement de son service worker est le préchargement de la navigation. L'utilisation du préchargement de navigation est un avantage clé en termes de performances pour tout service worker devant utiliser une réponse du réseau pour répondre aux requêtes de navigation. Elle indique au navigateur de commencer à envoyer la requête de navigation immédiatement, en même temps que le service worker:

Illustration du démarrage du logiciel effectué parallèlement à la requête de navigation.

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, le service ne devrait pas entraîner de surcharge de latence.

L'équipe de recherche devait également éviter d'avoir recours à 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. Étant donné qu'il n'existe pas de règle stricte pour déterminer ce qui constitue un appareil "bas de gamme", ils ont créé l'heuristique consistant à vérifier la RAM totale installée sur l'appareil. Les appareils inférieurs à 2 Go entrent dans la catégorie des appareils d'entrée de gamme, pour laquelle le temps de démarrage d'un service worker serait inacceptable.

L'espace de stockage disponible est un autre élément à prendre en compte, car l'ensemble complet 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 les tentatives de mise en cache des données présentent un risque d'échec en raison de l'échec des quotas de stockage.

L'équipe de recherche a donc dû définir plusieurs critères pour déterminer si un service worker devait ou non être utilisé: si un utilisateur accédait à la page de recherche Google à l'aide d'un navigateur compatible avec le préchargement de navigation, disposant d'au moins 2 gigaoctets de RAM et d'un espace de stockage suffisant, un service worker est enregistré. Les navigateurs ou les appareils qui ne répondent pas à ces critères n'ont pas accès à un service worker, mais ils peuvent toujours accéder à la même expérience de recherche Google.

L'un des avantages de cet enregistrement sélectif est la possibilité de livrer un service worker plus petit et plus efficace. Le ciblage de navigateurs assez modernes pour exécuter le code du service worker élimine les frais liés à la transpilation et aux polyfills pour les navigateurs plus anciens. Cela a permis de réduire environ 8 kilo-octets de code JavaScript non compressé par rapport à la taille totale de l'implémentation du service worker.

Problème: champs d'application des service workers

Une fois que l'équipe de recherche a effectué suffisamment de tests de latence et était convaincue que le préchargement de la navigation lui offrait une solution viable et sans latence pour l'utilisation d'un service worker, certains problèmes pratiques ont commencé à passer au premier plan. L'un de ces problèmes est lié aux règles de champ d'application du service worker. Le champ d'application d'un service worker détermine les pages dont il peut potentiellement prendre le contrôle.

La portée fonctionne en fonction du préfixe de chemin de l'URL. Pour les domaines qui hébergent une seule application Web, ce n'est pas un problème, car vous utilisez normalement simplement un service worker avec le champ d'application maximal de /, qui pourrait prendre le contrôle de n'importe quelle page du domaine. Mais la structure des URL de la recherche Google est un peu plus compliquée.

Si le service worker recevait le champ d'application maximal /, il pourrait prendre le contrôle de n'importe quelle page hébergée sous www.google.com (ou l'équivalent régional), et certaines URL de ce domaine n'ont rien à voir avec la recherche Google. Un champ d'application plus raisonnable et restrictif serait /search, ce qui éliminerait au moins les URL 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 d'URL déterminent le type de résultat de recherche spécifique à afficher. Certains de ces types utilisent un codebase complètement différent de celui de la page de résultats de recherche sur le Web classique. 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 (encore) prête à proposer leur propre expérience de service worker.

Solution: créer un framework de distribution et de routage

Bien qu'il existe certaines propositions permettant d'utiliser des paramètres plus puissants que les préfixes de chemin d'URL pour déterminer les champs d'application des service workers, l'équipe de recherche Google a dû déployer un service worker qui n'a rien fait pour un sous-ensemble de pages qu'elle contrôlait.

Pour contourner ce problème, l'équipe de recherche Google a créé un framework de distribution et de routage sur mesure, qui peut ê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 à supprimer. 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 Recherche d'images et Shopping Search, d'intégrer leur propre logique de service worker par la suite, si elles décident de la mettre en œuvre.

Problème: résultats et métriques personnalisés

Les utilisateurs peuvent se connecter à la recherche Google à l'aide de leur compte Google. L'expérience des résultats de recherche peut être personnalisée en fonction des données spécifiques de leur compte. Les utilisateurs connectés sont identifiés par des cookies de navigateur spécifiques. Il s'agit d'une norme reconnue et largement prise en charge.

Toutefois, les cookies de navigateur présentent un inconvénient : ils ne sont pas exposés à l'intérieur d'un service worker et il n'existe aucun moyen d'examiner automatiquement leurs valeurs pour s'assurer qu'elles n'ont pas changé suite à la déconnexion d'un utilisateur ou au changement de compte. (Des efforts sont en cours pour accorder l'accès aux cookies aux service workers, mais à l'heure où nous écrivons cette section, cette approche est expérimentale et n'est pas largement acceptée.)

Une incohérence entre la vue du service worker de l'utilisateur actuellement connecté et celle de l'utilisateur réel connecté à l'interface Web de la recherche Google peut entraîner une personnalisation incorrecte des résultats de recherche, ou des métriques et des journaux détournés. Chacun de ces scénarios de défaillance constituerait un problème sérieux pour l'équipe de recherche Google.

Solution: envoyer les 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 au sein d'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 pertinents et utilise postMessage() pour les envoyer au service worker.

Le service worker compare ensuite la valeur actuelle du cookie à la valeur attendue. En cas de non-concordance, il prend des mesures pour supprimer définitivement toutes les données spécifiques à l'utilisateur de son espace de stockage, puis actualise la page de résultats de recherche sans aucune personnalisation incorrecte.

Les étapes spécifiques suivies par le service worker pour rétablir une valeur de référence sont spécifiques 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 issues des cookies des navigateurs.

Problème: tests et dynamisme

Comme indiqué précédemment, l'équipe de la recherche Google s'appuie fortement sur l'exécution de tests en production et sur les tests en conditions réelles du nouveau code et des nouvelles fonctionnalités avant de les activer par défaut. Cela peut représenter un défi pour un service worker statique qui s'appuie en grande partie sur des données mises en cache, car l'activation ou la désactivation des tests par les utilisateurs nécessite 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é dynamiquement, personnalisé par le serveur Web pour chaque utilisateur, au lieu d'un script de service worker unique et statique 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 les scripts de ce service. La modification des ensembles d'expériences actives pour un utilisateur s'effectue à l'aide d'une combinaison de techniques traditionnelles, telles que les cookies de navigateur, et en diffusant du code mis à jour dans l'URL de service worker enregistrée.

L'utilisation d'un script de service worker généré dynamiquement facilite également la création d'un mécanisme de secours dans l'éventualité peu probable où l'implémentation d'un service worker rencontre un bug fatal à éviter. La réponse du nœud de calcul dynamique du serveur peut être une implémentation no-op, ce qui désactive le service worker pour tout ou partie des utilisateurs actuels.

Problème: coordonner les mises à jour

L'un des défis les plus difficiles pour un déploiement de service worker réel est de trouver un compromis raisonnable entre le choix d'éviter le réseau au profit du cache, tout en veillant à ce que les utilisateurs existants bénéficient des mises à jour et des 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 de longue durée qu'un utilisateur garde ouverte indéfiniment, sans accéder à de nouvelles pages.
  • Fréquence de déploiement des mises à jour de votre serveur Web backend.
  • Indique si l'utilisateur moyen tolère l'utilisation d'une version légèrement obsolète de votre application Web ou si l'actualisation est la priorité absolue.

Lors de ses tests avec les service workers, l'équipe de la recherche Google s'est assurée de continuer à exécuter les tests sur un certain nombre de mises à jour du backend planifiées, afin de s'assurer que les métriques et l'expérience utilisateur correspondent mieux à ce que les utilisateurs revenant y trouveraient 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 de recherche Google a constaté que la configuration suivante offrait le bon é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 ou 25 minutes) et est enregistrée avec updateViaCache définie sur "all" pour garantir le respect de l'en-tête. Comme vous pouvez l'imaginer, le backend Web de la recherche Google est un vaste ensemble de serveurs répartis dans le monde entier, qui nécessite une disponibilité la plus proche possible de 100 %. Le déploiement d'une modification qui affecterait le contenu du script de service worker s'effectue de manière progressive.

Si un utilisateur accède à un backend qui a été mis à jour, puis accède rapidement à une autre page qui rencontre un backend qui n'a pas encore reçu le nœud de calcul du service mis à jour, il risque de basculer plusieurs fois entre les versions. Par conséquent, demander au navigateur de ne rechercher 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. Ce comportement a pour avantage de réduire considérablement le trafic reçu par le point de terminaison qui génère dynamiquement le script de service worker.

De plus, un en-tête ETag est défini dans la réponse HTTP du script du service worker, garantissant ainsi 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 si aucune mise à jour du service worker n'a été déployée entre temps.

Bien que certaines interactions dans l'application Web de recherche Google utilisent des navigations de type application monopage (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 pour accélérer le cycle de mise à jour des service workers : clients.claim() et skipWaiting(). En cliquant sur l'interface de la recherche Google, vous accédez généralement à de nouveaux documents HTML. L'appel de skipWaiting garantit qu'un service worker mis à jour peut traiter 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 peut commencer à contrôler 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 nécessairement une solution adaptée à tous. Elle est le résultat de tests A/B minutieux sur différentes combinaisons d'options de diffusion, jusqu'à ce qu'elle trouve celle qui fonctionnait le mieux. 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 ignorant toujours le cache HTTP. Si vous créez une application monopage que les utilisateurs peuvent garder 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 dans le cache si vous autorisez le nouveau service worker à s'activer alors qu'il existe des clients de longue durée.

Points clés à retenir

Par défaut, les service workers ne nuisent pas aux performances

L'ajout d'un service worker à votre application Web implique l'insertion d'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 d'un cache local plutôt que du réseau, les frais généraux liés à l'exécution du service worker sont généralement négligeables par rapport à l'amélioration des performances résultant de la mise en cache. Toutefois, si vous savez que votre service worker doit toujours consulter le réseau pour traiter les requêtes de navigation, l'utilisation du préchargement de navigation constitue une amélioration cruciale des performances.

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

L'expérience des service workers s'est révélée bien plus prometteuse aujourd'hui qu'il y a un an. Tous les navigateurs modernes offrent désormais au moins une compatibilité avec les service workers. Malheureusement, certaines fonctionnalités avancées des service workers, telles que la synchronisation en arrière-plan et le préchargement de navigation, ne sont pas déployées de manière universelle. Une approche raisonnable consiste à vérifier le sous-ensemble spécifique de caractéristiques dont vous savez avoir besoin et à n'enregistrer un service worker que lorsqu'elles sont présentes.

De même, si vous avez effectué des tests et que vous savez que les appareils bas de gamme finissent par générer des performances médiocres en conduisant à 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 à considérer les service workers comme une amélioration progressive qui est ajoutée à une application Web lorsque toutes les conditions préalables sont remplies, et qu'ils améliorent l'expérience utilisateur et les performances de chargement globales.

Tout mesurer

Le seul moyen de déterminer si la livraison d'un service worker a eu un impact positif ou négatif sur l'expérience utilisateur est de tester et de mesurer les résultats.

Les spécificités de la configuration de mesures pertinentes dépendent du fournisseur de solutions d'analyse que vous utilisez et de la manière dont vous effectuez habituellement 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'utilisation des service workers dans l'application Web Google I/O.

Sans objectifs

Bien que de nombreux membres de la communauté des développeurs Web associent des service workers à des progressive web apps, la création d'une "PWA pour la recherche Google" n'était pas l'objectif initial de l'équipe. L'application Web de la recherche Google ne fournit actuellement pas de métadonnées via un fichier manifeste d'application Web et elle n'encourage pas les utilisateurs à suivre le parcours "Ajouter à l'écran d'accueil". L'équipe chargée de la recherche Google est actuellement satisfaite du fait que les utilisateurs accèdent à son application Web via les points d'entrée traditionnels de la recherche Google.

Plutôt que d'essayer de faire de l'expérience Web de la recherche Google l'équivalent de ce que vous attendez d'une application installée, l'objectif était d'améliorer progressivement le site Web existant lors du déploiement initial.

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é le contexte de rédaction de 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 (oct. 2021): Depuis la publication initiale de cet article, l'équipe de la recherche Google 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 de retrait. À mesure que l'infrastructure Web de la recherche Google évolue, l'équipe peut revoir la conception de son service worker.