Gérer la performance dans un projet de tag JavaScript : le récit d’un Product Manager #2.

Je suis Product Manager chez AB Tasty, et aujourd’hui je vous parle du projet d’amélioration des performances de notre tag JS.

Léo Wiel
15 min readJan 13, 2022

Le contenu que vous vous apprêtez à lire est la deuxième partie de l’article. Retrouvez la première partie ici !

Place à l’action

De ce que je vous ai raconté jusque là, seule une maigre portion était déjà dans nos esprits au lancement du projet. La majorité de nos connaissances d’aujourd’hui ont été acquises au fil du temps et continuent de s’agrandir. Nos projets n’en font que devenir de plus en plus impactants et visibles.

On s’est très rapidement fixé des objectifs qui allaient être les éléments constituants de notre boussole. Ils n’ont pas vraiment changé depuis le début et sont toujours les “métriques” qui pilotent nos réflexions et valident ou non nos projets. Les voici :

  • Un visiteur ne devrait télécharger et exécuter que du contenu qui va lui être utile (chaque octet utilisé est gâché)
  • Identifier les usages impactants mais légitimes d’AB Tasty et en proposer une alternative
  • Utiliser l’asynchrone et le dynamique autant que possible tout en gardant un oeil sur le flickering
  • Surveiller et rester informé sur les performances de notre tag chez un maximum d’utilisateurs et suivre les métriques importantes : CWV, Bootup time, CPU usage…
  • Fournir aux utilisateurs des insights pour qu’ils puissent surveiller et corriger les problèmes de performance par eux-mêmes.

Ces 5 objectifs ont été adressés tout au long du projet. Plus ou moins et pas tous en même temps, mais chaque projet, chaque fonctionnalité, chaque mise en prod’ a agit sur l’un de ces 4 piliers.

Délivrer avec précaution

Ceux qui utilisent AB Tasty depuis un petit moment savent (peut-être ?) que l’on a déjà fait une migration majeure de notre tag, les plus anciens savent même que nous en sommes à la troisième itération. De manière générale, n’importe qui ayant travaillé avec des outils versionnés connaît la difficulté de faire des migrations d’une version majeure à une autre.

Dans notre cas, ça se passe toujours bien pour 99% des utilisateurs. Pour le reste, des problèmes d’incompatibilité et de conséquences imprévues ternissent toujours le tableau.

De plus, les migrations c’est complexe et coûteux, en plus d’être risqué.

Dans le monde du développement, de plus en plus de systèmes suivent la logique d’intégration continue / déploiement continue (CI / CD dans le jargon). Le principe est simple : pas d’interruption, pas de migration, tout est fluide et transparent (les devs qui me lisent, ne me frappez pas, je simplifie au max).

Chez AB Tasty, nous avons une chance extraordinaire : depuis environ 2 ans maintenant, nous commercialisons sous la marque Flagship notre propre outil permettant de gérer des déploiements progressifs (progressive rollout et feature flagging). Non seulement l’outil est gratuit pour nous, mais en plus on a un accès direct à son support et à sa roadmap, miam !

On a pu donc, très vite, imaginer un système où on pourrait déployer nos optimisations et nouvelles features sans prendre de risque et avec un contrôle total sur ce qui se passe sur la production. Le système de déploiement par branche était né.

A partir de ce moment, nous maintenons 3 branches distinctes de notre tag, dont deux sont simplement des branches “en avance” sur la branche principale. Grâce à ça, quelqu’un de pas trop technique (comme moi) peut décider en quelques clics de souris et quelques touches de clavier quel utilisateur doit recevoir quelle branche du tag. Encore mieux, on peut déployer nos optimisations par paquet tout en gérant des exclusions. Bref, on maîtrise de bout en bout le déploiement de nos projets chez nos utilisateurs.

Les 3 branches sont les suivantes :

  • Latest, simplement la branche principale utilisée par tout le monde
  • Next, la branche Latest améliorée de nos derniers projets validés, fonctionnels mais qui peuvent présenter des désagréments et incompatibilités
  • Unstable, la branche de test interne qui contient nos toutes dernières évolutions et qui est potentiellement… Instable. On a déjà eu des utilisateurs qui se sont retrouvés dessus, mais c’est rare.

D’un point de vue des devs de mon équipe, il suffit de déclarer quelle fonctionnalité va sur chacune des branches, et la compilation fait le reste après avoir interrogé Flagship. Maintenant qu’on est rodé, c’est simple et sans couture.

Le Performance Center

Comme tous les autres êtres humains, mes journées ne durent que 24 heures. Je n’ai ni plus ni moins de temps que les autres, mais si je veux rester efficace 5 jours par semaine je dois me retirer du pied les épines les plus courantes.

Expliquer les raisons du manque de performance d’un tag est assez rébarbatif : il faut vérifier son poids, faire la liste des campagnes, comprendre leur contenu, … Bref, tout un tas de tâches pour lesquelles la machine est plus efficace que nous.

Dans les premiers mois de notre projet nous avons déployé le Centre de Performance, un outil assez iconique de notre travail puisqu’il est à la fois le plus visible et l’un des plus efficaces.

Chaque utilisateur d’AB Tasty a accès, dans les paramètres de son compte, à cette interface qui va lui présenter points par points tout ce qu’il doit optimiser pour s’assurer d’avoir un tag au top de sa forme.

On le fait évoluer régulièrement et on ajoute des critères assez souvents. L’objectif est que chaque utilisateur soit autonome dans sa compréhension de l’impact de ses usages et puisse prendre ses décisions de manière éclairée.

La particularité de cet outil est qu’il n’oblige à rien : il ne fait que présenter des faits factuels de type “la fonctionnalité X est activée, cela nuit aux bonnes performances de votre tag”. Mais si l’utilisateur décide qu’il a besoin de cette fonctionnalité, alors il peut la garder, tout en sachant que cela va dégrader les performances de son tag.

Aujourd’hui, l’interface prodigue des conseils selon 3 catégories :

  • Les problèmes : ce qui n’est vraiment pas normal et devrait être corrigé aussi vite que possible. C’est notamment le cas des campagnes lourdes (> 20 kB, on est sympa !)
  • Les optimisations : ce qui pourrait être amélioré sans faire de concessions. En général, il s’agit de couper des vieilles campagnes, de revoir des ciblages, …
  • Les optimisations facultatives : elle regroupe les fonctionnalités qui ont un coût en performance mais qui sont indispensables pour certains utilisateurs. On les affiche donc pour faire de la pédagogie, pour faire naître la réflexion du besoin de cette fonctionnalité, mais elle ne coûte pas dans le “score” global que la page indique.

La découpe du tag

L’objectif le plus évident et le plus trivial à entamer était celui des ressources inutiles. Pourquoi chaque visiteur individuel devrait-il télécharger du contenu qui ne lui est pas utile ?

Prenons un exemple très parlant. Si vous décidez d’activer une popin uniquement sur vos visiteurs desktop, pourquoi télécharger cette popin sur les visiteurs mobiles ?

Exactement.

Les campagnes différées

Nos utilisateurs l’ont connu sous le terme “campagnes différées” (deferred campaigns) et grâce à elle, une campagne n’est téléchargée que si le visiteur entre dans le critère de ciblage. S’il n’y rentre pas, la campagne n’existera jamais pour lui. C’est potentiellement quelques ko d’économisés multipliés par le nombre de campagnes actuellement en prod sur le compte. Dit autrement, c’est aussi la perspective de pouvoir mettre en prod davantage de campagnes sans craindre de voir exploser le poids de son tag.

Initialement accessible uniquement à la demande, campagne par campagne, car générateur de flickering, cette fonctionnalité a rapidement dû évoluer vers sa maturité.

Elle n’était d’abord recommandée que dans des cas précis : pour les campagnes de milieu voir bas de page ou pour les affichages asynchrones (contenu dynamique et widgets). En effet, dans le cas d’un affichage en haut de page, on observait clairement un risque supplémentaire de scintillement, le fameux effet de la page d’origine qui s’affiche quelques centaines de millisecondes avant sa variation.

C’était évidemment frustrant. Non seulement la fonctionnalité super impactante était restreinte à un nombre limité de campagnes, mais en plus elle était uniquement activable manuellement. Ajoutez à cela une communication un peu ratée sur le risque du flickering et elle devenait la fonctionnalité de l’exception plutôt que de la norme. Forcément, son adoption n’était pas au rendez-vous.

Avec l’équipe, on a pris le sujet à bras le corps. Cette fonctionnalité était décisive et devait être poussée au plus grand nombre. Mieux, elle devait devenir le standard.

Le sujet du flickering devait devenir un non-sujet.

On l’a optimisée, améliorée, travaillée aux petits oignons jusqu’à ce que le risque de flickering devienne négligeable. Puis, nous avons rendu son activation par défaut. A partir de maintenant, toute campagne créée sur AB Tasty est en mode différée et est donc téléchargée que lorsque nécessaire.

Fin Novembre 2021, un peu plus de 40% des campagnes actives chez nos clients sont différées. Ca paraît encore faible, mais c’est principalement parce qu’on a énormément d’anciennes campagnes qui tournent encore. Pour nous, l’adoption est totale, et très rares sont les campagnes récentes à utiliser ce mode. Mieux : je n’ai jamais entendu parler de problème de flickering dans des conditions normales.

Oui, j’ai utilisé le terme “conditions normales”, parce qu’évidemment, il y a un hic. Et ce hic illustre pas mal la complexité d’avoir un tag exécuté sur une variété virtuellement infinie de configurations.

Rapidement, des utilisateurs se sont plaints d’obtenir du flickering lorsque la visite se faisait sur une connexion lente. Logique, le téléchargement se faisant en asynchrone, il pouvait prendre quelques secondes à se faire et donc s’afficher tardivement.

Notre solution a été radicale : mettre un timeout sur ce téléchargement afin de l’interrompre totalement au bout de 3 secondes. C’était se priver d’une part du trafic, mais les conditions ne sont de toute façon pas réunies pour faire du test de manière efficace. Au contraire, nous favorisons dans ce cas la rapidité de la page.

Le revers de la médaille est très vite venue. Lorsque vous demandez à notre tag de faire une répartition 50/50, il va simplement jeter un dé à l’arrivée du visiteur. Si le dé indique 1, 2 ou 3, alors le visiteur ira sur l’original. S’il indique 4, 5 ou 6, il ira sur la variation. Lorsque la campagne est effectivement vue, notre tag l’enregistre dans son cookie et l’indique à notre moteur de collecte pour que le compteur de conversion soit incrémenté.

Seulement voilà, si la campagne différée met du temps à se télécharger, alors elle sera abandonnée et jamais vue par le visiteur. En revanche, le tag a bien fait sa répartition 50/50. Comme l’original est une variation vide, il n’y a pas de contenu à télécharger et elle sera donc toujours “affichée”. On a donc 100% des visiteurs affectés à l’original qui sont correctement enregistrés et x% des visiteurs affectés à la variation qui, eux, ne le sont pas. En résulte donc un déséquilibre et des questions embarrassantes de la part de nos utilisateurs.

On a bien pensé à rééquilibrer à la volée, après tout, on le fait déjà avec notre allocation dynamique du trafic. Mais d’après notre équipe de data scientists, cela ne ferait qu’introduire un biais à un problème qui n’en est pas un.

En parlant de biais…

On cherche toujours à aller le plus loin possible tout en procédant pas à pas. Puisque le téléchargement des campagnes de manière différée était un succès, pourquoi ne pas aller jusqu’à ne télécharger que la variation allouée ? Pour ce qui est de réduire encore le nombre de kilobytes qui se baladent sur le réseau, c’est une excellente idée, sur le papier.

Forcément, faire cela introduit un biais gigantesque : à partir du moment où on traite des populations de manière différente (l’une télécharge un fichier A, l’autre un fichier B et un dernier tiers ne télécharge rien du tout, soit l’original), comment peut-on s’imaginer comparer leurs performances de manière fiable ?

On ne peut pas, on a donc fait machine arrière pour éviter de sacrifier la fiabilité sur l’autel de la performance, le premier étant plus crucial que le second. Aujourd’hui, un visiteur télécharge toute la campagne avec toutes ses variations, qu’il soit affecté à l’original ou non.

Les campagnes différées étaient néanmoins un succès, mais il restait encore pas mal de choses à retirer du tag. On a donc très rapidement identifié deux parties de notre tag qui pouvaient très facilement être découpées et mises de côté.

Les imports dynamiques

Analytics.js est notre moteur de collecte de données. Sur chaque page, pour chaque visiteur, il collecte les actions et les pages vues du visiteur pour les envoyer à notre pipeline de collecte. Ces données servent à l’élaboration de nos reportings et segments et de manière générale à nourrir tous nos algos. C’est un moteur qui ne change que très rarement, en cas d’ajout d’une nouvelle fonctionnalité (rare) ou d’un bug à corriger (ce qui n’arrive jamais, n’est-ce pas ?).

L’intérêt de le mettre de côté est que l’on pouvait dès lors changer drastiquement sa politique de cache : de 30 secondes, les 8 ko de ce moteur peuvent maintenant vivre 365 jours dans le cache du navigateur sans qu’ils ne soient remis en question à chaque visite. Hop, 8 ko d’économisés à chaque page vue, ou presque !

Puis jquery.js. Avec AB Tasty, on ne vous le recommande pas, mais vous pouvez utiliser jQuery si vous le souhaitez pour créer vos variations. Vous avez ensuite le choix : soit vous utilisez votre propre librairie, soit vous utilisez celle d’AB Tasty. Dans ce dernier cas, elle sera bien évidemment intégrée au tag.

Exactement comme pour analytics.js, jquery.js est maintenant déconnecté du tag et est stocké dans le cache du navigateur. Il peut se passer des années sans que nous ne touchions à cette librairie, elle est maintenant stockée bien au chaud dans le navigateur de vos internautes et votre site vient de s’alléger d’environ 40 ko.

Jusqu’ici, les campagnes différées combinées aux imports dynamiques ont permis d’observer un gain significatif sur l’impact d’AB Tasty sur les perfs des clients que l’on monitorait.

Pourtant, une partie non négligeable du tag était encore inutilisée par une portion importante de notre trafic.

Graphique de répartition du JS inutilisé sur un exemple

Imaginez donc ma joie lorsque mon indispensable tech lead m’annonce qu’il a trouvé un moyen de rendre le tag 100% dynamique et ce de manière automatique et sans effort important (merci webpack, pour les curieux).

En quelques heures, un POC est monté, et le constat est sans appel : sans faire de magie vaudou particulière, le poids de notre tag initial tombe à 2 kB et 5 modules se créent automatiquement. On sait déjà qu’on va pouvoir aller encore plus loin, mais c’est une première étape qui s’annonce vraiment, vraiment, chouette.

Qu’on s’entende bien : cela signifie que grâce à ce système la partie dynamique téléchargée toutes les 30 secondes (pour garantir l’arrivée des modifications en temps et en heure sur le site) ne pèse plus que 2 kB, une broutille, tandis que tout le reste peut maintenant être caché 365 jours dans la navigateur ou jusqu’à sa modification. On me parle même d’aller plus loin et d’annuler totalement le besoin d’avoir un cache de 30 secondes sur le tag initial grâce à la magie du stale-while-revalidate.

Banco ! C’est un développement qui va être priorisé et mis sur la branche Next aussi vite que possible.

La boucle est bouclée

Imaginez que vous soyez Product Manager. Comment faites-vous pour retirer une fonctionnalité utilisée par l’immense majorité de vos utilisateurs et pour qui elle est indispensable au bon fonctionnement d’un outil qu’il paie ?

Vous avez 2 heures.

Moi j’ai eu bien plus que 2 heures, donc je vous livre ici une solution, la meilleure que j’ai pu trouver.

Un peu de contexte tout d’abord :

Les campagnes que nos utilisateurs conçoivent sont en majorité ciblées : elles ne se déclenchent que si un critère ou une combinaison de critères d’URL, de datalayer, de géolocalisation, de nombre de pages vues minimum, ou bien d’autres, sont validés.

Le web 2.0 étant ce qu’il est, certains critères ne sont vrais qu’après chargement de la page : à la suite d’une action du visiteur, par exemple. Il faut donc pouvoir détecter ces critères “après chargement”.

On a donc depuis toujours un curseur qui permet à l’utilisateur de définir s’il veut que son critère soit vérifié au chargement de la page ou bien “plus tard”.

Dans le premier cas, c’est facile. On vérifie à l’initialisation du tag, au “DOM Ready”, bref, à quelques moments clés du chargement de la page, on valide ou non le ciblage et on passe à autre chose. Si les critères n’étaient pas vrais à ce moment là, alors la campagne ne s’est pas exécutée et on tentera à nouveau à la prochaine page.

Dans le second cas, il n’existe pas de méthode vraiment universelle. La meilleure manière d’attendre c’est de boucler. Vérifier de manière périodique jusqu’à ce qu’on considère que ça n’en vaille plus la peine.

Horreur et damnation ! Rien de pire qu’une boucle JavaScript pour surconsommer les ressources CPU et faire hurler les ventilateurs du PC qui essaie tant bien que mal de tenir la charge (j’exagère un peu pour donner du corps à mon propos, vous avez l’idée).

Mais comment faire pour proposer à des utilisateurs en majorité issue de formation marketing (qui ne connaissent souvent pas grand chose au JS donc) une solution universelle qui leur permette de faire “ce qu’ils attendent” sans impacter négativement les performances de leur site ?

La réponse est dans la question.

La solution magique n’existe pas. Ou plutôt, le problème est exactement le fait de donner la possibilité à des utilisateurs qui ne saissient pas la portée exacte de leurs actions d’activer une option magique qui va tout faire pour eux.

Attention, je ne dis pas qu’AB Tasty ne devrait être utilisé que par des techos et que les marketeux devraient tous suivre une formation JS (même si, sans se mentir, ce serait pas mal pour des utilisateurs du web). Je dis simplement qu’il est probablement temps d’apporter des nuances dans ce qu’on considère comme du “user-friendly” ou encore du “WYSIWYG” (What You See Is What You Get).

On ne peut pas décemment imaginer concevoir une solution universelle dans un monde numérique dans lequel les sites web sont si différents, les technos si spécifiques et les usages si particuliers. J’ai peut-être tord, comme souvent, et dans ce cas je vous invite à venir m’en parler !

Ce qu’on peut faire, néanmoins, c’est prendre acte des usages, des bonnes pratiques et leur trouver des solutions qu’on épurera le plus possible afin d’en tirer des fonctionnalités simples à comprendre et à utiliser.

Je vous passe nos diverses tentatives d’optimiser la fonctionnalité existente. C’était évidemment la première étape : avant même de penser à la retirer, on s’est dit qu’on allait l’améliorer. C’était mieux, mais l’impact restait colossal. On a très vite compris qu’on n’arriverait pas à limiter la casse juste en l’optimisant.

On a donc acté le fait qu’on allait retirer cette fonctionnalité. On a regardé les statistiques, la quasi totalité de nos utilisateurs l’utilisaient, parfois intensivement. Ca n’allait pas être facile !

La meilleure méthode pour attaquer ce genre de projet n’est pas une surprise : confrontez vos utilisateurs avec la décision et voyez ce qui en ressort.

Avec l’équipe, on a réunit toute l’équipe de “Technical Support Engineer” d’AB Tasty. Ces super-héros sont les architectes de toutes les campagnes qui tournent chez les clients qui ont choisi de nous déléguer cette tâche. Ils produisent des centaines de campagne par semaine et sont donc les utilisateurs les plus intensifs de la fonctionnalité de ciblage. Leur retour était donc ultra précieux.

J’ai volontairement mis les pieds dans le plat immédiatement. Le titre de la réunion sur Google Agenda était “AJAX mode sunset and discussion” (“AJAX mode” étant le nom de code de la fonctionnalité en interne). J’ai pris les 15 premières minutes pour leur montrer un cas très précis de l’impact gigantesque de la fonctionnalité sur les performances d’un site client.

Il n’était pas question de leur laisser le choix ni de les laisser argumenter sur pourquoi ils s’opposeraient à une telle mesure, mais plutôt d’acter la décision et de leur poser la question : “en quoi cela va-t-il vous impacter ?”.

Ce riche échange nous a permis d’identifier précisément leurs usages (au pluriel !) de la fonctionnalité. En sortant de la réunion, on a pu cartographier exactement ce que nos utilisateurs faisaient avec elle et leur promettre de valider avec eux une alternative pour chaque usage, lorsque c’était possible.

Le roi est mort, longue vie au roi !

La fonctionnalité n’était pas morte, elle allait renaître de ses cendres.

J’arrête là les allégories, vous avez compris.

Pas question de supprimer la fonctionnalité tant que nous n’avions pas proposé une couverture fonctionnelle équivalente grâce au déploiement d’alternatives. L’objectif était de garantir les usages sans dégrader l’expérience utilisateur, quitte même à l’améliorer parfois.

Bien évidemment, nous avons dû nous séparer de quelques usages marginaux. Mais 99% sera conservé.

A l’heure où j’écris ces lignes, ça n’a pas encore été fait. Précisement parce que nous avons choisi de prendre notre temps et de nous assurer que les alternatives proposées sont toutes opérationnelles, efficaces et adoptées.

Nous avons aussi “choisi” de conserver la compatibilité. C’est un choix subit, car il était totalement inenvisageable de parcourir les dizaines de milliers de campagnes actives afin de les adapter aux nouvelles options. Cela signifie par contre que nous allons vivre avec cette fonctionnalité obsolète pendant plusieurs années, et je n’exagère pas, jusqu’à ce qu’elle s’éteigne naturellement. Nous avons réellement des utilisateurs qui gardent des campagnes actives pendant plusieurs années.

Allez hop, la conclusion de toute cette belle aventure sera dans une troisième courte et dernière partie !

--

--