frontend / react
Performance : memo et virtualisation
Explication
Ce que vous allez apprendre
- Savoir quand React devient réellement lent et pourquoi ce n'est pas systématique
- Empêcher un composant de se re-rendre inutilement avec
memo() - Comprendre pourquoi
memo()échoue silencieusement sans props stables - Virtualiser une longue liste pour ne monter dans le DOM que ce qui est visible
- Utiliser le Profiler React pour mesurer avant d'optimiser
Dans quel contexte ?
L'écran src/pages/AdminUsersPage.jsx affiche un tableau de 8000 utilisateurs et devient perceptiblement saccadé au scroll et à chaque frappe dans la barre de recherche du header, pourtant totalement indépendante du tableau. Le Profiler React révèle deux causes distinctes : chaque ligne se re-rend inutilement à cause d'une fonction recréée à chaque rendu, et les 8000 lignes sont montées dans le DOM même si seules 15 sont visibles à l'écran. Cette leçon règle les deux problèmes avec memo()/useCallback et la virtualisation.
Étape 1 : React est déjà rapide par défaut
Tant qu'une application reste petite, React est déjà rapide par défaut : la comparaison entre deux versions de l'interface est très optimisée.
Étape 2 : où les problèmes apparaissent
Les problèmes de performance apparaissent surtout à l'échelle : de grands tableaux de données, des composants qui se re-rendent en cascade sans raison visible.
Étape 3 : ce que fait vraiment memo()
memo() enveloppe un composant pour lui donner un réflexe simple : avant de le re-rendre, comparer ses nouvelles props aux précédentes ; si elles sont identiques, sauter complètement son rendu.
Étape 4 : la limite à bien comprendre
Cet outil ne fonctionne que si les props reçues restent réellement stables d'un rendu à l'autre, ce qui implique souvent de le combiner avec useMemo/useCallback côté parent.
Étape 5 : un autre problème, les longues listes
Pour une liste de plusieurs milliers d'éléments, même sans problème de re-rendu, monter tous ces éléments dans le DOM devient coûteux, car la majorité n'est jamais visible à l'écran.
Étape 6 : la solution, la virtualisation
La virtualisation consiste à ne monter réellement dans le DOM que les quelques éléments visibles à un instant donné, en simulant la hauteur totale avec un élément fantôme.
Piège fréquent
Envelopper un composant dans memo() sans stabiliser les props qu'il reçoit (fonctions recréées, objets littéraux) ne change strictement rien : les props sont "différentes" à chaque rendu, donc memo() ne peut jamais sauter le rendu.
| Symptôme observé | Cause probable | Solution |
|---|---|---|
| Toutes les lignes d'un tableau se re-rendent au moindre changement | Props non stables (fonction recréée) | memo() + useCallback |
| Scroll saccadé sur une liste de milliers d'items | Tous les items sont montés dans le DOM | Virtualisation |
La règle d'or à retenir
Aucune de ces techniques ne doit être appliquée par réflexe. La bonne méthode consiste toujours à mesurer d'abord, avec le Profiler de React.
Et ensuite ?
Une fois la performance de rendu comprise, la prochaine étape aborde un autre levier : ne charger le code JavaScript qu'au moment où il devient nécessaire, avec le code splitting.
Commandes & code
Performance : memo et virtualisation
Éviter de recalculer/re-rendre ce qui n'a pas changé, et ne jamais monter ce qui n'est pas visible.
import { memo } from "react";
// memo() : comparaison superficielle des props -- si identiques, saute le rendu du composant
const LigneUtilisateur = memo(function LigneUtilisateur({ utilisateur }) {
console.log("Rendu de", utilisateur.nom);
return <tr><td>{utilisateur.nom}</td><td>{utilisateur.email}</td></tr>;
});
// Comparateur custom : nécessaire si la comparaison superficielle par défaut ne suffit pas
const LigneOptimisee = memo(
function LigneOptimisee({ utilisateur }) {
return <tr><td>{utilisateur.nom}</td></tr>;
},
(prevProps, nextProps) => prevProps.utilisateur.id === nextProps.utilisateur.id
// retourne true si on veut SKIP le rendu (props "égales" selon cette logique)
);// memo() est inutile, voire contre-productif, si les props changent à chaque rendu de toute façon
function TableauUtilisateurs({ utilisateurs }) {
return (
<table>
<tbody>
{utilisateurs.map((u) => (
// si "onClick={() => faireChose(u.id)}" est recréé ici à chaque rendu du parent,
// LigneUtilisateur ne bénéficie JAMAIS de son memo() -- combiner avec useCallback
<LigneUtilisateur key={u.id} utilisateur={u} />
))}
</tbody>
</table>
);
}// Virtualisation : ne monter dans le DOM que les lignes VISIBLES, pas les 10 000 items
import { useState, useRef, useMemo } from "react";
function ListeVirtualisee({ items, hauteurLigne = 40, hauteurConteneur = 400 }) {
const [scrollTop, setScrollTop] = useState(0);
const indexDebut = Math.floor(scrollTop / hauteurLigne);
const nbVisibles = Math.ceil(hauteurConteneur / hauteurLigne) + 2; // +2 : marge de sécurité (overscan)
const indexFin = Math.min(items.length, indexDebut + nbVisibles);
const itemsVisibles = useMemo(
() => items.slice(indexDebut, indexFin),
[items, indexDebut, indexFin]
);
return (
<div
style={{ height: hauteurConteneur, overflowY: "auto", position: "relative" }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
{/* div fantôme : donne la hauteur TOTALE au scroll, comme si tout était rendu */}
<div style={{ height: items.length * hauteurLigne, position: "relative" }}>
{itemsVisibles.map((item, i) => (
<div
key={item.id}
style={{
position: "absolute",
top: (indexDebut + i) * hauteurLigne,
height: hauteurLigne,
width: "100%",
}}
>
{item.nom}
</div>
))}
</div>
</div>
);
}
// en production : préférer une librairie mature (react-window, @tanstack/react-virtual)
// qui gère overscan dynamique, tailles variables, et scroll horizontal// Profiler : mesurer AVANT d'optimiser, jamais l'inverse
import { Profiler } from "react";
function surRendu(id, phase, dureeReelle) {
console.log(`${id} (${phase}) : ${dureeReelle.toFixed(2)}ms`);
}
function App() {
return (
<Profiler id="TableauUtilisateurs" onRender={surRendu}>
<TableauUtilisateurs utilisateurs={utilisateurs} />
</Profiler>
);
}Résumé
memo()compare les props superficiellement : n'a d'effet que si elles restent référentiellement stables.- Combiner
memo()avecuseCallback/useMemocôté parent, sinon les props "changent" à chaque rendu. - La virtualisation ne monte dans le DOM que les lignes visibles, indispensable au-delà de quelques centaines d'items.
- Toujours profiler avant d'optimiser : la mémoïsation prématurée ajoute de la complexité pour un gain parfois nul.
Exercices pratiques
Mission : désaturer le tableau de 8000 utilisateurs
Objectif : Corriger un memo() inefficace faute de props stables, et virtualiser une longue liste pour éliminer le scroll saccadé.
Contexte
Le Profiler React confirme sur AdminUsersPage.jsx que taper dans la barre de recherche du header, totalement indépendante du tableau, re-rend quand même les 8000 LigneUtilisateur enveloppées dans memo(). Le composant parent recrée onClick={() => ouvrirDetail(u.id)} à chaque rendu directement dans le .map(). De plus, les 8000 <tr> sont toutes montées dans le DOM alors que seules 15 lignes sont visibles à l'écran, ce qui rend le scroll saccadé.