ENFR

FoodChéri & Seazon · React, Vanilla Extract · Performance

Rendre la grille aussi rapide que ses données

  • React
  • Vanilla Extract
  • React.memo
  • React DevTools
  • Lighthouse
  • ~60 %de re-renders en moins
  • ~40 %de chargement en moins

Le problème, en une phrase

L’écran au cœur de tout l’abonnement (la grille où les clients choisissent leurs repas) était lent. Les données étaient déjà là. C’est l’interface qui n’arrivait pas à suivre.

Contexte

Sur FoodChéri comme sur Seazon, choisir ses repas est l’action centrale. Chaque abonné voit une grille de 24 à 50 cartes de repas et compose sa semaine à partir de là. Si cette grille est lente, le moment le plus important du produit devient lent. Et c’était le cas. Les cartes étaient saccadées à l’usage, la grille mettait trop de temps à devenir utilisable, et cette latence se trouvait pile sur le chemin de la conversion.

Le plus étrange : le serveur n’était pas en cause. Les données des repas arrivaient vite. Quelque chose se passait mal après le chargement des données, côté client, pendant le render.

Trouver la vraie cause

Je n’optimise pas au jugé, alors j’ai ouvert le profiler de React DevTools et enregistré une interaction avec la grille.

Le flamegraph était clair. Chacune des 24 à 50 cartes se re-rendait bien plus que nécessaire, et chaque render était coûteux. Deux problèmes empilés : trop de renders, et chaque render faisant trop de travail.

La partie « trop de travail » a été la surprise. Le coût venait de la façon dont les cartes étaient stylées. Elles utilisaient react-jss (une librairie CSS-in-JS à l’exécution) avec createUseStyles() qui lisait des valeurs depuis les props du composant. Comme ces props changeaient à chaque render (nouvelles références d’objets, état des repas qui évolue), le hook de style recalculait les styles pour chaque carte, à chaque render. Avec une carte, c’est invisible. Avec cinquante cartes qui se re-rendent encore et encore, ça devient tout le problème.

Une grille de 6×6 cartes de repas : dans l’état avant, toutes les cartes sont surlignées en rouge, montrant qu’elles se re-rendent toutes ; dans l’état après, seules trois sont surlignées en vert

La vraie cause n’était donc pas « React est lent » ou « on a trop de cartes ». C’était que la solution de style faisait un travail coûteux à l’exécution, à chaque render, et que ce coût était multiplié sur une grande grille répétée.

La décision, et celle que j’ai écartée

La solution rapide évidente était la virtualisation : ne rendre que les cartes visibles à l’écran et recycler les autres. C’est la réponse standard à « grande grille, render lent », et elle aurait montré une amélioration immédiate.

J’ai choisi de ne pas le faire.

La virtualisation aurait masqué le problème, pas résolu. Rendre moins de cartes baisse le coût total. Mais chaque carte reste trop coûteuse individuellement. Dès que l’utilisateur scrolle, le coût revient. Et la virtualisation n’est pas gratuite : elle ajoute de la complexité de scroll, peut nuire à l’accessibilité et à la recherche dans la page, et complique une mise en page qui n’en avait pas besoin. J’aurais ajouté une complexité permanente pour cacher un coût que je pouvais supprimer.

Le meilleur choix était de rendre chaque carte peu coûteuse. La grille serait alors rapide avec ou sans virtualisation. J’ai fait deux changements, un pour chaque problème :

  1. Remplacé react-jss par Vanilla Extract. Vanilla Extract génère le CSS au build, sans coût à l’exécution. Les styles recalculés à chaque render ont tout simplement cessé d’être calculés au moment du render. Cela a supprimé le coût par carte et par render, à la source.

  2. Mémoïsé les composants de carte avec React.memo. Une fois que les styles ne forçaient plus de travail à chaque render, j’ai fait en sorte que les cartes ne se re-rendent que lorsque leurs propres données changeaient, pas parce qu’un parent ou un voisin avait changé. Cela a réglé la moitié « trop de renders ».

Les deux correctifs se complétaient volontairement. L’un rend chaque render peu coûteux. L’autre rend les renders rares. N’en corriger qu’un aurait laissé la moitié du coût en place.

Résultats

  • ~60 % de re-renders en moins sur la grille, mesuré dans le profiler de React DevTools (le même flamegraph, avant et après).
  • ~40 % de chargement en moins sur ce parcours, dans le cadre de la réduction globale du temps de chargement mesurée dans Lighthouse.
  • La grille est devenue réactive sur l’écran précis qui compte le plus : celui que chaque abonné utilise pour acheter.

Et parce que le correctif a supprimé la cause au lieu de la cacher, la grille est restée rapide à mesure que le nombre de cartes augmentait, sans machinerie de virtualisation à maintenir.

Ce que je ferais différemment

Je l’aurais détecté plus tôt. Le coût du CSS-in-JS à l’exécution était invisible à petite échelle et n’est apparu qu’une fois la grille agrandie. Un performance budget, ou une étape de profiling en revue de code, l’aurait signalé avant la production. Le correctif était propre. La leçon était simple : « ça marche maintenant » et « ça marchera à l’échelle » sont deux affirmations différentes, et une UI répétée mérite qu’on vérifie la seconde.