Pourquoi vos optimisations ralentissent votre app ?
React fournit trois outils de memoisation pour optimiser les performances de vos applications : React.memo, useMemo et useCallback. Chacun intervient a un niveau different du cycle de rendu. Comprendre quand et pourquoi les utiliser est essentiel pour ecrire du code React performant sans tomber dans le piege de la sur-optimisation.
Le trio de la memoisation
React propose trois mecanismes complementaires. Chacun resout un probleme specifique dans le pipeline de rendu.
React.memo
Higher-Order Component qui empeche le re-render d'un composant si ses props n'ont pas change. Agit au niveau du composant entier.
useMemo
Hook qui met en cache le resultat d'un calcul entre les renders. Agit au niveau d'une valeur calculee.
useCallback
Hook qui met en cache une definition de fonction entre les renders. Agit au niveau d'une reference de fonction.
Pourquoi la memoisation existe-t-elle ?
Par defaut, quand un composant parent se re-rend, tous ses enfants se re-rendent egalement, meme si leurs props n'ont pas change. Dans la plupart des cas, React est suffisamment rapide pour que cela ne pose aucun probleme. Mais lorsqu'un composant effectue un calcul couteux, rend une longue liste, ou passe des callbacks a des enfants complexes, ces re-renders inutiles peuvent degrader l'experience utilisateur.
1// Probleme : ce composant se re-rend a chaque changement du parent2// meme si "user" n'a pas change3
4interface UserCardProps {5 user: { name: string; email: string };6}7
8function UserCard({ user }: UserCardProps) {9 // Ce console.log s'affiche a CHAQUE render du parent10 console.log('UserCard render:', user.name);11
12 // Imaginons un calcul couteux ici...13 const formattedDate = new Intl.DateTimeFormat('fr-FR', {14 dateStyle: 'full',15 timeStyle: 'long',16 }).format(new Date());17
18 return (19 <div className="p-4 border rounded-lg">20 <h3>{user.name}</h3>21 <p>{user.email}</p>22 <span>{formattedDate}</span>23 </div>24 );25}26
27// Le parent : chaque clic sur le bouton re-rend UserCard28function Dashboard() {29 const [count, setCount] = useState(0);30 const user = { name: 'Alice Dupont', email: 'alice@example.com' };31
32 return (33 <div>34 {/* Ce bouton modifie count, pas user */}35 <button onClick={() => setCount(c => c + 1)}>36 Compteur : {count}37 </button>38
39 {/* Pourtant UserCard se re-rend a chaque clic */}40 <UserCard user={user} />41 </div>42 );43}Ce que vous apprendrez dans ce guide
Un parcours complet pour maitriser la memoisation React, de la theorie a la pratique en production.
- 01Comprendre le mecanisme de re-render et identifier quand il devient problematique
- 02Maitriser React.memo et la comparaison superficielle des props
- 03Utiliser useMemo pour les calculs couteux et la stabilisation de references
- 04Appliquer useCallback pour stabiliser les fonctions passees en props
- 05Combiner les trois outils dans un scenario complet de production
- 06Eviter les erreurs courantes et savoir quand ne pas memoiser
- 07Decouvrir le React Compiler et l'avenir de la memoisation automatique
Qu'est-ce qui déclenche vraiment un re-render ?
Avant de memoiser quoi que ce soit, il faut comprendre comment React decide de re-rendre un composant. Le mecanisme est simple : quand un etat change, React re-execute le composant et tous ses descendants. Mais re-render ne signifie pas forcement re-paint du DOM reel. Cette distinction est fondamentale.
Le cycle de rendu React
Quand un composant appelle setState, React declenche une cascade : le composant se re-execute, produit un nouveau Virtual DOM, React compare l'ancien et le nouveau (reconciliation), puis applique uniquement les differences au DOM reel. Les enfants suivent le meme processus, meme si leurs props sont identiques.
1// Demonstration : la cascade de re-renders2
3function App() {4 const [count, setCount] = useState(0);5 console.log('App render'); // S'affiche a chaque clic6
7 return (8 <div>9 <button onClick={() => setCount(c => c + 1)}>10 Incrementer ({count})11 </button>12 <Parent />13 </div>14 );15}16
17function Parent() {18 console.log(' Parent render'); // S'affiche aussi !19 return (20 <div>21 <Child />22 <Sibling />23 </div>24 );25}26
27function Child() {28 console.log(' Child render'); // S'affiche aussi !29 return <p>Je suis Child</p>;30}31
32function Sibling() {33 console.log(' Sibling render'); // S'affiche aussi !34 return <p>Je suis Sibling</p>;35}36
37// Console apres un clic sur "Incrementer" :38// App render39// Parent render40// Child render41// Sibling render42//43// Tous les composants se re-executent, meme si44// aucun d'entre eux ne recoit "count" en prop.Re-render et re-paint : deux choses differentes
Un re-render React (execution de la fonction composant) ne signifie pas que le navigateur va redessiner les pixels. React compare le Virtual DOM ancien et nouveau, et ne touche au DOM reel que si quelque chose a change.
Re-render (Virtual DOM)
- La fonction composant est re-executee
- Un nouveau Virtual DOM est produit
- React compare ancien et nouveau
- Cout : execution JavaScript en memoire
Re-paint (DOM reel)
- Le navigateur redessine des pixels
- Ne se produit que si le DOM est modifie
- React applique le diff minimal
- Cout : layout, paint, composite du navigateur
1// Exemple concret : composant "lourd" avec un calcul couteux2
3interface ProductListProps {4 products: Product[];5 category: string;6}7
8function ProductList({ products, category }: ProductListProps) {9 console.log('ProductList render - debut du calcul...');10
11 // Simulation d'un traitement couteux : filtrage + tri + transformation12 // Sur 10 000 produits, ce calcul prend ~50ms13 const filteredProducts = products14 .filter(p => p.category === category)15 .sort((a, b) => b.rating - a.rating)16 .map(p => ({17 ...p,18 displayPrice: new Intl.NumberFormat('fr-FR', {19 style: 'currency',20 currency: 'EUR',21 }).format(p.price),22 slug: p.name.toLowerCase().replace(/\s+/g, '-'),23 }));24
25 console.log(`ProductList render - ${filteredProducts.length} produits traites`);26
27 return (28 <ul>29 {filteredProducts.map(product => (30 <li key={product.id}>31 {product.name} - {product.displayPrice}32 </li>33 ))}34 </ul>35 );36}37
38// Parent : le compteur de notifications n'a RIEN a voir avec les produits39function StorePage() {40 const [notifications, setNotifications] = useState(0);41 const [products] = useState<Product[]>(generateProducts(10000));42
43 return (44 <div>45 <header>46 <button onClick={() => setNotifications(n => n + 1)}>47 Notifications ({notifications})48 </button>49 </header>50
51 {/* Ce composant refait le calcul couteux a chaque clic52 sur le bouton notifications, pour rien. */}53 <ProductList products={products} category="electronique" />54 </div>55 );56}Re-render n'est pas toujours un probleme
React est extremement rapide. La reconciliation du Virtual DOM est optimisee pour traiter des milliers de composants en quelques millisecondes. Ne memoiser que lorsque vous avez mesure un probleme reel.
- --Un composant simple (texte, image, layout) se re-rend en moins de 0,1 ms
- --React ne met a jour le DOM reel que si le Virtual DOM a effectivement change
- --La memoisation a un cout : comparaison des deps, memoire supplementaire, complexite du code
- --Regle d'or : mesurer d'abord avec le Profiler, optimiser ensuite
1// Identifier les re-renders couteux avec React DevTools Profiler2//3// Etape 1 : Ouvrir React DevTools > onglet "Profiler"4// Etape 2 : Cliquer sur "Record" (bouton rond)5// Etape 3 : Effectuer l'interaction a analyser (clic, saisie, etc.)6// Etape 4 : Cliquer sur "Stop"7// Etape 5 : Analyser le flamegraph8
9// Ce que vous voyez dans le flamegraph :10//11// [App] 0.2ms12// [StorePage] 0.3ms13// [Header] 0.1ms14// [ProductList] 47.3ms <-- PROBLEME ICI15// [ProductCard] x 84216//17// La barre "ProductList" est jaune/rouge = composant lent18// Les composants gris n'ont pas ete re-rendus19//20// Criteres pour decider d'optimiser :21// - Composant > 16ms (1 frame a 60fps) = a optimiser22// - Composant > 5ms avec interactions frequentes = a surveiller23// - Composant < 1ms = ne pas toucher24
25// Alternative programmatique : React.Profiler26import { Profiler } from 'react';27
28function onRenderCallback(29 id: string,30 phase: 'mount' | 'update',31 actualDuration: number, // temps de render en ms32 baseDuration: number, // temps sans memoisation33 startTime: number,34 commitTime: number,35) {36 if (actualDuration > 16) {37 console.warn(38 `[Perf] ${id} a pris ${actualDuration.toFixed(1)}ms (${phase})`39 );40 }41}42
43// Utilisation :44<Profiler id="ProductList" onRender={onRenderCallback}>45 <ProductList products={products} category="electronique" />46</Profiler>Quand les re-renders deviennent un vrai probleme
Les re-renders deviennent problematiques dans trois scenarios precis : lorsqu'un composant effectue un calcul couteux a chaque render (filtrage, tri, formatage de grandes listes), lorsqu'un composant rend un grand nombre d'enfants (liste de centaines d'elements), ou lorsque les re-renders se produisent a haute frequence (frappe clavier, mouvement de souris, scroll). Dans le reste des cas, faites confiance a React : il est deja optimise pour gerer les re-renders courants.
Comment éviter les re-renders inutiles de composants ?
React.memo est un Higher-Order Component (HOC) qui enveloppe un composant et empeche son re-render si ses props n'ont pas change. C'est l'outil le plus visible du trio de memoisation : il agit au niveau du composant entier et decide si l'execution de la fonction composant peut etre sautee.
1// Syntaxe de base : envelopper un composant avec React.memo2
3import { memo } from 'react';4
5interface UserCardProps {6 name: string;7 email: string;8 role: string;9}10
11// Sans memo : se re-rend a CHAQUE render du parent12function UserCard({ name, email, role }: UserCardProps) {13 console.log('UserCard render:', name);14 return (15 <div className="p-4 border rounded-lg">16 <h3 className="font-bold">{name}</h3>17 <p className="text-gray-600">{email}</p>18 <span className="text-sm bg-blue-100 px-2 py-1 rounded">{role}</span>19 </div>20 );21}22
23// Avec memo : ne se re-rend QUE si name, email ou role changent24const MemoizedUserCard = memo(function UserCard({25 name,26 email,27 role,28}: UserCardProps) {29 console.log('MemoizedUserCard render:', name);30 return (31 <div className="p-4 border rounded-lg">32 <h3 className="font-bold">{name}</h3>33 <p className="text-gray-600">{email}</p>34 <span className="text-sm bg-blue-100 px-2 py-1 rounded">{role}</span>35 </div>36 );37});38
39// Export direct (pattern le plus courant)40export default memo(UserCard);Comparaison superficielle (shallow comparison)
Par defaut, React.memo effectue une comparaison superficielle des props : il verifie l'egalite par reference (===) pour chaque prop. Pour les types primitifs (string, number, boolean), cela fonctionne parfaitement. Pour les objets et fonctions, c'est la que les pieges commencent.
1// CAS 1 : Props primitives - React.memo FONCTIONNE parfaitement2
3import { memo, useState } from 'react';4
5const ExpensiveChart = memo(function ExpensiveChart({6 title,7 value,8 showLegend,9}: {10 title: string; // Primitive : comparaison par valeur11 value: number; // Primitive : comparaison par valeur12 showLegend: boolean; // Primitive : comparaison par valeur13}) {14 console.log('ExpensiveChart render');15 // Simulation d'un rendu couteux (graphique SVG complexe)16 const paths = Array.from({ length: 1000 }, (_, i) => (17 <path key={i} d={`M${i} ${Math.sin(i) * value}`} />18 ));19
20 return (21 <div>22 <h3>{title}</h3>23 <svg>{paths}</svg>24 {showLegend && <Legend />}25 </div>26 );27});28
29function Dashboard() {30 const [notifications, setNotifications] = useState(0);31
32 return (33 <div>34 <button onClick={() => setNotifications(n => n + 1)}>35 Notifications ({notifications})36 </button>37
38 {/* "Revenue mensuelle", 42000 et true ne changent pas39 entre les renders -> ExpensiveChart ne se re-rend PAS */}40 <ExpensiveChart41 title="Revenue mensuelle" // "Revenue mensuelle" === "Revenue mensuelle" -> true42 value={42000} // 42000 === 42000 -> true43 showLegend={true} // true === true -> true44 />45 {/* Resultat : le clic sur Notifications ne declenche PAS46 le re-render de ExpensiveChart */}47 </div>48 );49}1// CAS 2 : Objets et fonctions inline - React.memo est CONTOURNE2
3import { memo, useState } from 'react';4
5const UserProfile = memo(function UserProfile({6 user,7 onEdit,8}: {9 user: { name: string; age: number };10 onEdit: () => void;11}) {12 console.log('UserProfile render'); // S'affiche a CHAQUE render du parent !13 return (14 <div>15 <h3>{user.name}, {user.age} ans</h3>16 <button onClick={onEdit}>Modifier</button>17 </div>18 );19});20
21function App() {22 const [count, setCount] = useState(0);23
24 return (25 <div>26 <button onClick={() => setCount(c => c + 1)}>27 Compteur : {count}28 </button>29
30 <UserProfile31 // PROBLEME 1 : objet literal = nouvelle reference a chaque render32 // { name: 'Alice', age: 30 } !== { name: 'Alice', age: 30 }33 // car ce sont deux objets differents en memoire34 user={{ name: 'Alice', age: 30 }}35
36 // PROBLEME 2 : fonction inline = nouvelle reference a chaque render37 // () => {} !== () => {}38 // car ce sont deux fonctions differentes en memoire39 onEdit={() => console.log('edit')}40 />41 {/* memo compare les props par reference (===)42 Objet precedent !== Nouvel objet -> re-render43 Fonction precedente !== Nouvelle fonction -> re-render44 Resultat : memo est completement inutile ici */}45 </div>46 );47}Comparateur personnalise (areEqual)
Pour les cas ou la comparaison superficielle ne suffit pas, React.memo accepte un second argument : une fonction de comparaison personnalisee. Cette fonction recoit les anciennes et nouvelles props et retourne true si le composant ne doit PAS se re-rendre.
1// Comparateur personnalise : controle fin sur la comparaison2
3import { memo } from 'react';4
5interface DataGridProps {6 data: Array<{ id: string; value: number; label: string }>;7 columns: string[];8 onRowClick: (id: string) => void;9 lastUpdated: Date;10}11
12// areEqual retourne true = PAS de re-render13// areEqual retourne false = re-render14function areEqual(prevProps: DataGridProps, nextProps: DataGridProps): boolean {15 // Comparer la longueur et les IDs des donnees (pas la reference)16 if (prevProps.data.length !== nextProps.data.length) return false;17
18 // Verifier si les donnees ont reellement change (deep comparison ciblee)19 const dataChanged = prevProps.data.some((item, index) => {20 const next = nextProps.data[index];21 return item.id !== next.id || item.value !== next.value;22 });23 if (dataChanged) return false;24
25 // Les colonnes changent rarement, comparer par longueur + contenu26 if (prevProps.columns.length !== nextProps.columns.length) return false;27 if (prevProps.columns.some((col, i) => col !== nextProps.columns[i])) return false;28
29 // Ignorer onRowClick (sera stabilise par useCallback dans le parent)30 // Ignorer lastUpdated si la difference est < 1 seconde31 const timeDiff = Math.abs(32 nextProps.lastUpdated.getTime() - prevProps.lastUpdated.getTime()33 );34 if (timeDiff > 1000) return false;35
36 return true; // Props considerees identiques, pas de re-render37}38
39const DataGrid = memo(function DataGrid({40 data,41 columns,42 onRowClick,43 lastUpdated,44}: DataGridProps) {45 console.log('DataGrid render -', data.length, 'lignes');46
47 return (48 <table>49 <thead>50 <tr>51 {columns.map(col => <th key={col}>{col}</th>)}52 </tr>53 </thead>54 <tbody>55 {data.map(row => (56 <tr key={row.id} onClick={() => onRowClick(row.id)}>57 <td>{row.label}</td>58 <td>{row.value}</td>59 </tr>60 ))}61 </tbody>62 </table>63 );64}, areEqual); // <-- second argument : la fonction de comparaison65
66export default DataGrid;React.memo est un HOC, pas un hook
Contrairement a useMemo et useCallback, React.memo n'est pas un hook. C'est un Higher-Order Component : une fonction qui prend un composant et retourne un nouveau composant. Cette distinction est importante.
HOC (React.memo)
- Enveloppe un composant entier
- Decide si le render est necessaire
- S'applique a l'exterieur du composant
- Fonctionne avec les composants classes et fonctions
- Compare les props entre deux renders
Hooks (useMemo, useCallback)
- Agissent a l'interieur d'un composant
- Mettent en cache des valeurs ou fonctions
- S'utilisent dans le corps de la fonction composant
- Fonctionnent uniquement avec les composants fonctions
- Comparent un tableau de dependances
Quand mémoiser un calcul coûteux ?
useMemo est un hook qui met en cache le resultat d'un calcul entre les renders. Il re-execute le calcul uniquement lorsque ses dependances changent. Ses deux cas d'usage principaux : eviter des calculs couteux et stabiliser des references d'objets pour que React.memo fonctionne correctement.
1// Cas d'usage 1 : memoiser un calcul couteux2
3import { useMemo, useState } from 'react';4
5interface Product {6 id: string;7 name: string;8 price: number;9 category: string;10 rating: number;11 inStock: boolean;12}13
14function ProductCatalog({ products }: { products: Product[] }) {15 const [search, setSearch] = useState('');16 const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');17 const [showInStockOnly, setShowInStockOnly] = useState(false);18
19 // Sans useMemo : ce calcul s'execute a CHAQUE render20 // Avec 10 000 produits, c'est ~50ms a chaque frappe clavier21 const filteredAndSorted = useMemo(() => {22 console.log('Recalcul de la liste filtree...');23
24 // Etape 1 : Filtrer par recherche25 let result = products.filter(p =>26 p.name.toLowerCase().includes(search.toLowerCase())27 );28
29 // Etape 2 : Filtrer par stock30 if (showInStockOnly) {31 result = result.filter(p => p.inStock);32 }33
34 // Etape 3 : Trier35 result.sort((a, b) => {36 if (sortBy === 'price') return a.price - b.price;37 return b.rating - a.rating;38 });39
40 // Etape 4 : Formater les prix (couteux avec Intl)41 return result.map(p => ({42 ...p,43 displayPrice: new Intl.NumberFormat('fr-FR', {44 style: 'currency',45 currency: 'EUR',46 }).format(p.price),47 }));48 }, [products, search, sortBy, showInStockOnly]);49 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^50 // Le calcul ne se relance QUE si l'une de ces 4 valeurs change.51 // Si un autre state du composant change (ex: un modal ouvert),52 // useMemo retourne le resultat en cache.53
54 return (55 <div>56 <input57 value={search}58 onChange={e => setSearch(e.target.value)}59 placeholder="Rechercher..."60 />61 <select62 value={sortBy}63 onChange={e => setSortBy(e.target.value as 'price' | 'rating')}64 >65 <option value="price">Prix</option>66 <option value="rating">Note</option>67 </select>68 <label>69 <input70 type="checkbox"71 checked={showInStockOnly}72 onChange={e => setShowInStockOnly(e.target.checked)}73 />74 En stock uniquement75 </label>76
77 <p>{filteredAndSorted.length} resultats</p>78 <ul>79 {filteredAndSorted.map(p => (80 <li key={p.id}>{p.name} - {p.displayPrice}</li>81 ))}82 </ul>83 </div>84 );85}Le piege des references : pourquoi useMemo est indispensable pour React.memo
En JavaScript, deux objets avec le meme contenu ne sont pas egaux par reference : { color: 'blue' } !== { color: 'blue' }. A chaque render, un objet literal cree une nouvelle reference. Si cet objet est passe en prop a un composant enveloppe par React.memo, le memo est contourne car la reference a change. useMemo resout ce probleme.
1// Le piege de la reference : sans useMemo, React.memo est inutile2
3import { memo, useMemo, useState } from 'react';4
5// Composant enfant memoize6const ChartConfig = memo(function ChartConfig({7 config,8}: {9 config: { theme: string; animate: boolean; gridLines: number };10}) {11 console.log('ChartConfig render');12 return (13 <div>14 Theme: {config.theme}, Grille: {config.gridLines} lignes15 </div>16 );17});18
19// PROBLEME : sans useMemo20function DashboardBroken() {21 const [refreshCount, setRefreshCount] = useState(0);22
23 // Cet objet est recree a CHAQUE render = nouvelle reference24 // { theme: 'dark', ... } !== { theme: 'dark', ... } par reference25 const config = { theme: 'dark', animate: true, gridLines: 5 };26
27 return (28 <div>29 <button onClick={() => setRefreshCount(r => r + 1)}>30 Rafraichir ({refreshCount})31 </button>32 {/* memo est contourne : config est une nouvelle reference */}33 <ChartConfig config={config} />34 </div>35 );36}37
38// SOLUTION : avec useMemo39function DashboardFixed() {40 const [refreshCount, setRefreshCount] = useState(0);41
42 // useMemo retourne la MEME reference tant que les deps ne changent pas43 const config = useMemo(44 () => ({ theme: 'dark', animate: true, gridLines: 5 }),45 [] // Pas de dependances = reference stable pour toute la vie du composant46 );47
48 return (49 <div>50 <button onClick={() => setRefreshCount(r => r + 1)}>51 Rafraichir ({refreshCount})52 </button>53 {/* memo fonctionne : config est la meme reference */}54 <ChartConfig config={config} />55 </div>56 );57}1// Exemple complet : liste de 10 000 elements avec filtre memoize2
3import { memo, useMemo, useState } from 'react';4
5interface Employee {6 id: string;7 name: string;8 department: string;9 salary: number;10 hireDate: string;11}12
13// Composant ligne memoize (ne re-rend que si l'employe change)14const EmployeeRow = memo(function EmployeeRow({15 employee,16 isSelected,17 onSelect,18}: {19 employee: Employee;20 isSelected: boolean;21 onSelect: (id: string) => void;22}) {23 console.log('EmployeeRow render:', employee.name);24 return (25 <tr26 className={isSelected ? 'bg-blue-50' : ''}27 onClick={() => onSelect(employee.id)}28 >29 <td>{employee.name}</td>30 <td>{employee.department}</td>31 <td>32 {new Intl.NumberFormat('fr-FR', {33 style: 'currency',34 currency: 'EUR',35 }).format(employee.salary)}36 </td>37 <td>{new Date(employee.hireDate).toLocaleDateString('fr-FR')}</td>38 </tr>39 );40});41
42function EmployeeDirectory({ employees }: { employees: Employee[] }) {43 const [filter, setFilter] = useState('');44 const [selectedId, setSelectedId] = useState<string | null>(null);45 const [sortField, setSortField] = useState<'name' | 'salary'>('name');46
47 // useMemo : le filtre + tri ne se recalcule que lorsque48 // employees, filter ou sortField changent.49 // Cliquer sur une ligne (selectedId change) ne relance PAS le calcul.50 const visibleEmployees = useMemo(() => {51 console.log('Recalcul de visibleEmployees...');52
53 const filtered = employees.filter(emp =>54 emp.name.toLowerCase().includes(filter.toLowerCase()) ||55 emp.department.toLowerCase().includes(filter.toLowerCase())56 );57
58 return filtered.sort((a, b) => {59 if (sortField === 'name') return a.name.localeCompare(b.name);60 return b.salary - a.salary;61 });62 }, [employees, filter, sortField]);63 // selectedId n'est PAS dans les deps -> cliquer ne recalcule pas64
65 return (66 <div>67 <input68 value={filter}69 onChange={e => setFilter(e.target.value)}70 placeholder="Filtrer par nom ou departement..."71 />72 <p>{visibleEmployees.length} employes affiches sur {employees.length}</p>73 <table>74 <thead>75 <tr>76 <th onClick={() => setSortField('name')}>Nom</th>77 <th>Departement</th>78 <th onClick={() => setSortField('salary')}>Salaire</th>79 <th>Embauche</th>80 </tr>81 </thead>82 <tbody>83 {visibleEmployees.map(emp => (84 <EmployeeRow85 key={emp.id}86 employee={emp}87 isSelected={emp.id === selectedId}88 onSelect={setSelectedId}89 />90 ))}91 </tbody>92 </table>93 </div>94 );95}useMemo cache le RESULTAT, pas la fonction
Distinction essentielle : useMemo execute la fonction et cache ce qu'elle retourne. useCallback, lui, cache la fonction elle-meme sans l'executer. Confondre les deux est une erreur frequente.
useMemo
Cache le resultat du calcul
useMemo(() => expensiveCalc(data), [data])Retourne : la valeur calculee (ex: un tableau filtre, un objet formate)
useCallback
Cache la fonction elle-meme
useCallback((id) => handleClick(id), [])Retourne : la reference de la fonction (pas son resultat)
Pourquoi vos callbacks cassent React.memo ?
useCallback met en cache une definition de fonction entre les renders. Contrairement a useMemo qui cache un resultat, useCallback cache la fonction elle-meme. Son utilite principale : stabiliser les references de fonctions passees en props a des enfants memoises ou utilisees dans les dependances de useEffect.
1// Syntaxe de base : useCallback2
3import { useCallback, useState } from 'react';4
5function TodoApp() {6 const [todos, setTodos] = useState<Todo[]>([]);7 const [inputValue, setInputValue] = useState('');8
9 // Sans useCallback : cette fonction est recreee a chaque render10 // const handleAddTodo = () => { ... };11
12 // Avec useCallback : la meme reference est reutilisee13 // tant que les dependances ne changent pas14 const handleAddTodo = useCallback(() => {15 if (!inputValue.trim()) return;16
17 setTodos(prev => [18 ...prev,19 { id: crypto.randomUUID(), text: inputValue, done: false },20 ]);21 setInputValue('');22 }, [inputValue]);23 // ^^^^^^^^^ Depend de inputValue car on le lit dans le callback.24 // Quand inputValue change, la fonction est recreee avec la nouvelle valeur.25
26 const handleToggle = useCallback((id: string) => {27 setTodos(prev =>28 prev.map(todo =>29 todo.id === id ? { ...todo, done: !todo.done } : todo30 )31 );32 }, []);33 // ^^ Pas de dependance : on utilise la forme fonctionnelle de setState34 // donc on n'a pas besoin de lire "todos" directement.35 // Cette reference reste stable pour TOUTE la vie du composant.36
37 const handleDelete = useCallback((id: string) => {38 setTodos(prev => prev.filter(todo => todo.id !== id));39 }, []);40
41 return (42 <div>43 <input value={inputValue} onChange={e => setInputValue(e.target.value)} />44 <button onClick={handleAddTodo}>Ajouter</button>45 <TodoList46 todos={todos}47 onToggle={handleToggle}48 onDelete={handleDelete}49 />50 </div>51 );52}Equivalence avec useMemo
useCallback(fn, deps) est strictement equivalent a useMemo(() => fn, deps). Le hook useCallback est un raccourci syntaxique fourni par React pour un cas d'usage tres courant : memoiser une fonction plutot qu'une valeur.
1// Equivalence useCallback / useMemo2
3import { useCallback, useMemo } from 'react';4
5function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {6 const [query, setQuery] = useState('');7
8 // Ces deux lignes sont STRICTEMENT equivalentes :9
10 // Version useCallback (syntaxe dediee, plus lisible)11 const handleSearch = useCallback(12 () => onSearch(query),13 [onSearch, query]14 );15
16 // Version useMemo (generique, meme resultat)17 const handleSearchAlt = useMemo(18 () => () => onSearch(query), // useMemo retourne la fonction19 [onSearch, query]20 );21
22 // handleSearch === handleSearchAlt (meme comportement)23 // Preferez useCallback pour les fonctions : plus explicite et lisible.24
25 return (26 <div>27 <input value={query} onChange={e => setQuery(e.target.value)} />28 <button onClick={handleSearch}>Rechercher</button>29 </div>30 );31}Sans consommateur memoise, useCallback seul est inutile
useCallback ne sert a rien si la fonction n'est pas passee a un composant enveloppe par React.memo ou utilisee dans un tableau de dependances (useEffect, useMemo). Sans consommateur qui tire parti de la reference stable, vous ajoutez de la complexite sans gain.
- --useCallback + React.memo = le composant enfant ne se re-rend pas (utile)
- --useCallback + useEffect deps = l'effet ne se relance pas (utile)
- --useCallback seul, sans consommateur = overhead sans benefice (inutile)
1// ANTI-PATTERN : useCallback sans React.memo2
3import { useCallback, useState } from 'react';4
5function ParentBroken() {6 const [count, setCount] = useState(0);7
8 // useCallback stabilise la reference de handleClick...9 const handleClick = useCallback(() => {10 console.log('clicked');11 }, []);12
13 return (14 <div>15 <button onClick={() => setCount(c => c + 1)}>16 Compteur : {count}17 </button>18
19 {/* MAIS ChildComponent n'est PAS memoize !20 Il se re-rend a chaque changement de count,21 que handleClick soit stable ou non.22 useCallback ne fait rien d'utile ici. */}23 <ChildComponent onClick={handleClick} />24 </div>25 );26}27
28// ChildComponent se re-rend a chaque render du parent29// car il n'est pas enveloppe par React.memo30function ChildComponent({ onClick }: { onClick: () => void }) {31 console.log('ChildComponent render'); // Toujours affiche32 return <button onClick={onClick}>Action</button>;33}34
35// -----------------------------------------------36
37// PATTERN CORRECT : useCallback + React.memo38
39import { memo } from 'react';40
41function ParentFixed() {42 const [count, setCount] = useState(0);43
44 const handleClick = useCallback(() => {45 console.log('clicked');46 }, []);47
48 return (49 <div>50 <button onClick={() => setCount(c => c + 1)}>51 Compteur : {count}52 </button>53
54 {/* MemoizedChild est enveloppe par memo,55 et handleClick est une reference stable.56 MemoizedChild ne se re-rend PAS quand count change. */}57 <MemoizedChild onClick={handleClick} />58 </div>59 );60}61
62const MemoizedChild = memo(function MemoizedChild({63 onClick,64}: {65 onClick: () => void;66}) {67 console.log('MemoizedChild render'); // Seulement au mount68 return <button onClick={onClick}>Action</button>;69});1// useCallback avec useEffect : eviter les boucles infinies2
3import { useCallback, useEffect, useState } from 'react';4
5interface SearchResults {6 items: Array<{ id: string; title: string }>;7 total: number;8}9
10function SearchPage({ apiBase }: { apiBase: string }) {11 const [query, setQuery] = useState('');12 const [results, setResults] = useState<SearchResults | null>(null);13
14 // La fonction de recherche depend de apiBase (prop) et query (state)15 const fetchResults = useCallback(async () => {16 if (!query.trim()) {17 setResults(null);18 return;19 }20
21 const response = await fetch(22 `${apiBase}/search?q=${encodeURIComponent(query)}`23 );24 const data: SearchResults = await response.json();25 setResults(data);26 }, [apiBase, query]);27 // fetchResults est recree UNIQUEMENT quand apiBase ou query changent28
29 // useEffect depend de fetchResults30 // Grace a useCallback, cet effet ne se relance que quand31 // apiBase ou query changent (pas a chaque render)32 useEffect(() => {33 const debounceTimer = setTimeout(() => {34 fetchResults();35 }, 300);36
37 return () => clearTimeout(debounceTimer);38 }, [fetchResults]);39 // Sans useCallback sur fetchResults :40 // fetchResults serait une nouvelle reference a chaque render41 // -> useEffect se relancerait a chaque render42 // -> requete API a chaque render = boucle infinie potentielle43
44 return (45 <div>46 <input47 value={query}48 onChange={e => setQuery(e.target.value)}49 placeholder="Rechercher..."50 />51 {results && (52 <div>53 <p>{results.total} resultats</p>54 <ul>55 {results.items.map(item => (56 <li key={item.id}>{item.title}</li>57 ))}58 </ul>59 </div>60 )}61 </div>62 );63}Comment combiner React.memo, useMemo et useCallback ?
Individuellement, chaque outil de memoisation a un role precis. Mais leur veritable puissance se revele quand ils travaillent ensemble. Cette section presente un scenario complet de production ou React.memo, useMemo et useCallback collaborent pour optimiser un tableau de bord avec des interactions frequentes.
Le scenario : un tableau de bord analytique
Imaginons un tableau de bord avec un compteur de notifications en temps reel, une liste de 5 000 transactions filtrable, et un graphique statistique couteux a rendre. L'utilisateur clique frequemment sur le bouton de notifications. Sans memoisation, chaque clic relance le filtrage des 5 000 lignes et le rendu du graphique.
1// VERSION SANS MEMOISATION2// Chaque clic sur "notifications" re-rend TOUT3
4import { useState } from 'react';5
6interface Transaction {7 id: string;8 label: string;9 amount: number;10 date: string;11 category: 'income' | 'expense';12}13
14// ---------- Composant Parent ----------15function AnalyticsDashboard() {16 const [notifications, setNotifications] = useState(0);17 const [filter, setFilter] = useState<'all' | 'income' | 'expense'>('all');18 const [transactions] = useState<Transaction[]>(19 generateTransactions(5000) // 5 000 transactions20 );21
22 // PROBLEME 1 : Ce calcul se relance a CHAQUE render23 // y compris quand on clique sur notifications24 const filteredTransactions = transactions25 .filter(t => filter === 'all' || t.category === filter)26 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())27 .map(t => ({28 ...t,29 displayAmount: new Intl.NumberFormat('fr-FR', {30 style: 'currency',31 currency: 'EUR',32 }).format(t.amount),33 }));34 // ~45ms pour 5000 elements35
36 // PROBLEME 2 : Nouvel objet a chaque render37 const stats = {38 total: filteredTransactions.reduce((sum, t) => sum + t.amount, 0),39 count: filteredTransactions.length,40 average: filteredTransactions.length > 041 ? filteredTransactions.reduce((sum, t) => sum + t.amount, 0) / filteredTransactions.length42 : 0,43 };44
45 // PROBLEME 3 : Nouvelle fonction a chaque render46 const handleTransactionClick = (id: string) => {47 console.log('Transaction selectionnee:', id);48 };49
50 console.log('Dashboard render - tout se re-execute');51
52 return (53 <div>54 <header>55 <button onClick={() => setNotifications(n => n + 1)}>56 Notifications ({notifications})57 </button>58 <select59 value={filter}60 onChange={e => setFilter(e.target.value as typeof filter)}61 >62 <option value="all">Toutes</option>63 <option value="income">Revenus</option>64 <option value="expense">Depenses</option>65 </select>66 </header>67
68 {/* Tout se re-rend a chaque clic sur notifications */}69 <StatsChart stats={stats} />70 <TransactionList71 transactions={filteredTransactions}72 onItemClick={handleTransactionClick}73 />74 </div>75 );76}77
78// Ces composants se re-rendent systematiquement79function StatsChart({ stats }: { stats: { total: number; count: number; average: number } }) {80 console.log('StatsChart render - rendu SVG couteux...');81 // Rendu d'un graphique SVG complexe (~30ms)82 return <div>Graphique : {stats.count} transactions</div>;83}84
85function TransactionList({86 transactions,87 onItemClick,88}: {89 transactions: Array<Transaction & { displayAmount: string }>;90 onItemClick: (id: string) => void;91}) {92 console.log('TransactionList render -', transactions.length, 'lignes');93 return (94 <ul>95 {transactions.map(t => (96 <li key={t.id} onClick={() => onItemClick(t.id)}>97 {t.label} : {t.displayAmount}98 </li>99 ))}100 </ul>101 );102}103
104// Console apres clic sur Notifications :105// Dashboard render - tout se re-execute (~45ms calcul)106// StatsChart render - rendu SVG couteux... (~30ms rendu)107// TransactionList render - 5000 lignes (~20ms rendu)108// TOTAL : ~95ms par clic = lag perceptible1// VERSION AVEC MEMOISATION2// Chaque outil a un role precis dans l'optimisation3
4import { memo, useCallback, useMemo, useState } from 'react';5
6interface Transaction {7 id: string;8 label: string;9 amount: number;10 date: string;11 category: 'income' | 'expense';12}13
14// ---------- Composant Parent ----------15function AnalyticsDashboard() {16 const [notifications, setNotifications] = useState(0);17 const [filter, setFilter] = useState<'all' | 'income' | 'expense'>('all');18 const [transactions] = useState<Transaction[]>(19 generateTransactions(5000)20 );21
22 // SOLUTION 1 : useMemo pour le calcul couteux23 // Ne recalcule QUE quand transactions ou filter changent24 // Cliquer sur notifications ne relance PAS ce calcul25 const filteredTransactions = useMemo(() => {26 console.log('Recalcul filteredTransactions...');27 return transactions28 .filter(t => filter === 'all' || t.category === filter)29 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())30 .map(t => ({31 ...t,32 displayAmount: new Intl.NumberFormat('fr-FR', {33 style: 'currency',34 currency: 'EUR',35 }).format(t.amount),36 }));37 }, [transactions, filter]);38
39 // SOLUTION 2 : useMemo pour stabiliser la reference objet40 // stats est derive de filteredTransactions41 // Meme reference tant que filteredTransactions ne change pas42 const stats = useMemo(() => ({43 total: filteredTransactions.reduce((sum, t) => sum + t.amount, 0),44 count: filteredTransactions.length,45 average: filteredTransactions.length > 046 ? filteredTransactions.reduce((sum, t) => sum + t.amount, 0) / filteredTransactions.length47 : 0,48 }), [filteredTransactions]);49
50 // SOLUTION 3 : useCallback pour stabiliser la reference fonction51 // handleTransactionClick garde la meme reference entre les renders52 const handleTransactionClick = useCallback((id: string) => {53 console.log('Transaction selectionnee:', id);54 }, []);55
56 console.log('Dashboard render');57
58 return (59 <div>60 <header>61 <button onClick={() => setNotifications(n => n + 1)}>62 Notifications ({notifications})63 </button>64 <select65 value={filter}66 onChange={e => setFilter(e.target.value as typeof filter)}67 >68 <option value="all">Toutes</option>69 <option value="income">Revenus</option>70 <option value="expense">Depenses</option>71 </select>72 </header>73
74 {/* React.memo empeche le re-render si les props sont stables */}75 <MemoizedStatsChart stats={stats} />76 <MemoizedTransactionList77 transactions={filteredTransactions}78 onItemClick={handleTransactionClick}79 />80 </div>81 );82}83
84// SOLUTION 4 : React.memo sur les composants enfants85// Ne se re-rend que si stats change (reference stable grace a useMemo)86const MemoizedStatsChart = memo(function StatsChart({87 stats,88}: {89 stats: { total: number; count: number; average: number };90}) {91 console.log('StatsChart render');92 return <div>Graphique : {stats.count} transactions</div>;93});94
95// Ne se re-rend que si transactions ou onItemClick changent96const MemoizedTransactionList = memo(function TransactionList({97 transactions,98 onItemClick,99}: {100 transactions: Array<Transaction & { displayAmount: string }>;101 onItemClick: (id: string) => void;102}) {103 console.log('TransactionList render -', transactions.length, 'lignes');104 return (105 <ul>106 {transactions.map(t => (107 <li key={t.id} onClick={() => onItemClick(t.id)}>108 {t.label} : {t.displayAmount}109 </li>110 ))}111 </ul>112 );113});114
115// Console apres clic sur Notifications :116// Dashboard render (~0.3ms)117//118// C'est TOUT. StatsChart et TransactionList ne se re-rendent PAS.119// useMemo retourne les valeurs en cache, useCallback la meme reference.120// React.memo compare les props : identiques -> pas de re-render.121//122// Console apres changement de filtre :123// Dashboard render124// Recalcul filteredTransactions... (~45ms, necessaire)125// StatsChart render (~30ms, necessaire)126// TransactionList render - 3200 lignes (~15ms, necessaire)127// Les re-renders sont limites aux cas ou les donnees changent reellement.Trace d'execution : que se passe-t-il exactement ?
Quand l'utilisateur clique sur 'Notifications', voici le cheminement precis de React a travers le code optimise.
Etape 1 : setState declenche le render
setNotifications(n => n + 1) marque AnalyticsDashboard pour re-render. React re-execute la fonction composant.
Etape 2 : useMemo verifie ses dependances
[transactions, filter] n'ont pas change (notifications n'est pas dans les deps). useMemo retourne le resultat en cache sans recalculer. Meme chose pour stats.
Etape 3 : useCallback retourne la meme reference
Les dependances de handleTransactionClick ([]) n'ont pas change. useCallback retourne la meme reference de fonction.
Etape 4 : React.memo compare les props
MemoizedStatsChart : stats est la meme reference -> pas de re-render. MemoizedTransactionList : transactions et onItemClick sont les memes references -> pas de re-render. Seul le header se met a jour pour afficher le nouveau compteur.
Comparaison des performances
Sur ce scenario avec 5 000 transactions, le gain est significatif. Sans memoisation, chaque clic sur le bouton de notifications coute environ 95ms (calcul + rendu graphique + rendu liste). Avec memoisation, le meme clic coute environ 0,3ms : seul le composant parent se re-execute, et les enfants sont sautes. C'est un facteur d'amelioration de plus de 300x sur cette interaction specifique.
React.memo vs useMemo vs useCallback : lequel utiliser ?
Les trois outils de memoisation React repondent a des besoins differents mais complementaires. Ce tableau comparatif synthetise leurs forces, limites et cas d'usage pour vous aider a choisir le bon outil selon le contexte.
- Evite les re-renders couteux des composants enfants
- Simple a appliquer (envelopper le composant)
- Fonctionne avec les composants fonctionnels et classes
- Shallow comparison par defaut (objets/fonctions inline contournent le memo)
- Inutile si les props changent a chaque render
- Overhead de comparaison des props a chaque render du parent
- •Composants lourds en rendu (graphiques, listes longues)
- •Listes avec beaucoup d'items (chaque item memoize)
- •Composants recevant des props stables (primitives, refs useMemo/useCallback)
- Evite les recalculs couteux (filtrage, tri, formatage)
- Stabilise les references d'objets pour React.memo
- Type-safe avec TypeScript (inference automatique)
- Consomme de la memoire (cache le resultat precedent)
- Comparaison des dependances a chaque render
- Peut masquer des problemes d'architecture (state derive mal concu)
- •Calculs complexes : filtrage, tri, aggregation de grandes listes
- •Derived state : valeur calculee a partir de props ou state
- •Stabiliser les props objet pour que React.memo fonctionne
- Stabilise les references de fonctions pour React.memo
- Evite les re-renders inutiles des enfants memoises
- Essentiel pour les dependances de useEffect
- Inutile sans consommateur memoize (React.memo ou useEffect)
- Closure stale si les dependances sont incorrectes
- Overhead de memoisation (comparaison des deps)
- •Callbacks passes a des enfants enveloppes par React.memo
- •Fonctions dans les dependances de useEffect
- •Event handlers stables pour des composants optimises
React.memo | useMemo | useCallback |
|---|---|---|
HOC qui empeche le re-render si les props sont identiques (shallow comparison par defaut). | Hook qui cache le resultat d'un calcul entre les renders. Re-execute uniquement quand les dependances changent. | Hook qui cache une definition de fonction entre les renders. La reference reste stable tant que les dependances ne changent pas. |
Avantages
| Avantages
| Avantages
|
Inconvenients
| Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
| Cas d'usage
|
Regles pratiques : quand utiliser quoi
Un guide de decision rapide pour choisir le bon outil de memoisation selon votre situation.
Utilisez React.memo quand...
- Le composant est couteux a rendre (graphique, longue liste, SVG complexe)
- Le composant recoit souvent les memes props
- Le parent se re-rend frequemment pour des raisons sans rapport avec l'enfant
Utilisez useMemo quand...
- Un calcul prend plus de quelques millisecondes (mesure avec le Profiler)
- Vous devez stabiliser une reference objet pour un enfant memoise
- Vous derivez des donnees complexes a partir de state ou props
Utilisez useCallback quand...
- La fonction est passee en prop a un enfant enveloppe par React.memo
- La fonction est dans le tableau de dependances d'un useEffect
- La fonction est passee a un hook tiers qui compare les references
Ne memoisez PAS quand...
- Le composant est leger et rapide a rendre (< 1ms)
- Les props changent presque a chaque render
- Vous n'avez pas mesure de probleme de performance reel
- Le composant est un noeud feuille sans enfants complexes
La chaine de memoisation
Pour que la memoisation soit efficace, chaque maillon de la chaine doit etre en place. Un seul maillon manquant et toute l'optimisation est perdue.
Maillon 1 : Le composant enfant est enveloppe par React.memo
Maillon 2 : Les props objet sont stabilisees par useMemo
Maillon 3 : Les props fonction sont stabilisees par useCallback
Resultat : le composant enfant ne se re-rend que lorsque ses donnees changent reellement.
Comparez React.memo, useMemo et useCallback avec des mesures reelles de temps de rendu sur un benchmark de liste de produits.
Quelles erreurs éviter avec la mémoisation ?
La memoisation est un outil puissant, mais mal utilisee, elle peut degrader les performances, introduire des bugs subtils ou simplement ajouter de la complexite sans aucun benefice. Voici les quatre erreurs les plus frequentes, avec pour chacune le code problematique et la correction.
Erreur 1 : Tout memoiser par defaut
Envelopper chaque composant avec React.memo et chaque valeur avec useMemo est un reflexe courant mais contre-productif. Chaque memoisation a un cout : comparaison des dependances, memoire pour le cache, complexite du code. Si le composant est leger ou si les props changent a chaque render, ce cout depasse le benefice.
1// ANTI-PATTERN : tout memoiser sans raison2
3import { memo, useCallback, useMemo } from 'react';4
5// Memoiser un composant trivial : aucun benefice6const Label = memo(function Label({ text }: { text: string }) {7 // Ce composant rend un seul <span> : cout de render < 0.01ms8 // Le cout de comparaison des props par memo est du meme ordre9 return <span className="text-sm text-gray-600">{text}</span>;10});11
12function FormField({ label, value, onChange }: FormFieldProps) {13 // Memoiser un calcul trivial : plus couteux que le calcul lui-meme14 const trimmedValue = useMemo(() => value.trim(), [value]);15 // value.trim() prend ~0.001ms16 // La comparaison des deps de useMemo prend ~0.001ms aussi17 // Aucun gain, mais code plus complexe18
19 // useCallback sans consommateur memoise : overhead pur20 const handleChange = useCallback(21 (e: React.ChangeEvent<HTMLInputElement>) => {22 onChange(e.target.value);23 },24 [onChange]25 );26 // handleChange est passe a <input>, un element natif27 // Les elements natifs ne beneficient PAS de React.memo28 // Cette memoisation ne sert strictement a rien29
30 return (31 <div>32 <Label text={label} />33 <input value={trimmedValue} onChange={handleChange} />34 </div>35 );36}1// CORRECTION : memoiser uniquement ce qui en a besoin2
3function FormField({ label, value, onChange }: FormFieldProps) {4 // Pas de useMemo : value.trim() est instantane5 const trimmedValue = value.trim();6
7 // Pas de useCallback : <input> est un element natif8 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {9 onChange(e.target.value);10 };11
12 return (13 <div>14 {/* Pas de memo : Label est trivial */}15 <span className="text-sm text-gray-600">{label}</span>16 <input value={trimmedValue} onChange={handleChange} />17 </div>18 );19}20
21// Regle : ne memoiser que lorsque le Profiler montre un probleme22// ou quand un enfant React.memo a besoin de references stablesErreur 2 : Oublier des dependances (stale closures)
Quand une fonction memorisee par useCallback ou useMemo lit une variable mais ne la liste pas dans ses dependances, elle capture une valeur obsolete. C'est le bug de la "stale closure" : la fonction fonctionne avec des donnees du passe.
1// BUG : stale closure - la fonction utilise une valeur obsolete2
3import { useCallback, useState } from 'react';4
5function Chat() {6 const [messages, setMessages] = useState<string[]>([]);7 const [draft, setDraft] = useState('');8
9 // BUG : draft n'est PAS dans les dependances10 const handleSend = useCallback(() => {11 if (!draft.trim()) return;12
13 // draft est capture au moment de la creation du callback14 // Si l'utilisateur tape "Bonjour" puis "Comment ca va",15 // handleSend enverra toujours la valeur de draft au premier render16 setMessages(prev => [...prev, draft]); // draft est toujours ''17 setDraft('');18 }, []); // <-- PROBLEME : deps vides, draft n'est jamais mis a jour19
20 return (21 <div>22 <ul>23 {messages.map((msg, i) => <li key={i}>{msg}</li>)}24 </ul>25 <input value={draft} onChange={e => setDraft(e.target.value)} />26 <button onClick={handleSend}>Envoyer</button>27 </div>28 );29}30
31// Execution pas a pas :32// 1. Render initial : draft = '', handleSend capture draft = ''33// 2. L'utilisateur tape "Bonjour" : draft = 'Bonjour'34// 3. handleSend est TOUJOURS la version du render initial35// 4. L'utilisateur clique sur Envoyer36// 5. handleSend lit draft... qui vaut '' (la valeur capturee)37// 6. Le message envoye est vide !1// CORRECTION : inclure draft dans les dependances2
3function Chat() {4 const [messages, setMessages] = useState<string[]>([]);5 const [draft, setDraft] = useState('');6
7 // FIX 1 : Ajouter draft aux dependances8 const handleSend = useCallback(() => {9 if (!draft.trim()) return;10 setMessages(prev => [...prev, draft]); // draft est a jour11 setDraft('');12 }, [draft]); // <-- draft dans les deps : la fonction se recree quand draft change13
14 // FIX 2 (alternative) : utiliser un ref pour les valeurs qui changent souvent15 // Utile quand vous voulez une reference stable ET une valeur a jour16 const draftRef = useRef(draft);17 draftRef.current = draft; // Mis a jour a chaque render18
19 const handleSendStable = useCallback(() => {20 if (!draftRef.current.trim()) return;21 setMessages(prev => [...prev, draftRef.current]);22 setDraft('');23 }, []); // Pas besoin de draft dans les deps : on lit la ref24
25 return (26 <div>27 <ul>28 {messages.map((msg, i) => <li key={i}>{msg}</li>)}29 </ul>30 <input value={draft} onChange={e => setDraft(e.target.value)} />31 <button onClick={handleSend}>Envoyer</button>32 </div>33 );34}Erreur 3 : React.memo sans stabiliser les props
Envelopper un composant avec React.memo mais continuer a lui passer des objets et fonctions inline en props est l'une des erreurs les plus frequentes. Le memo est contourne a chaque render car les references changent. Vous payez le cout de la comparaison sans aucun benefice.
1// PROBLEME : memo contourne par des props inline2
3import { memo, useState } from 'react';4
5// Le composant EST memoize...6const ExpensiveList = memo(function ExpensiveList({7 items,8 config,9 onItemClick,10}: {11 items: string[];12 config: { pageSize: number; showHeader: boolean };13 onItemClick: (item: string) => void;14}) {15 console.log('ExpensiveList render -', items.length, 'items');16 return (17 <ul>18 {items.map(item => (19 <li key={item} onClick={() => onItemClick(item)}>{item}</li>20 ))}21 </ul>22 );23});24
25function PageBroken() {26 const [theme, setTheme] = useState('light');27 const items = ['React', 'Vue', 'Angular', 'Svelte'];28
29 return (30 <div>31 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>32 Theme : {theme}33 </button>34
35 <ExpensiveList36 items={items}37 // Nouvel objet a chaque render -> memo contourne38 config={{ pageSize: 20, showHeader: true }}39 // Nouvelle fonction a chaque render -> memo contourne40 onItemClick={(item) => console.log('Clic:', item)}41 />42 {/* Resultat : ExpensiveList se re-rend a chaque43 changement de theme malgre React.memo */}44 </div>45 );46}1// SOLUTION : stabiliser les references avec useMemo et useCallback2
3import { memo, useCallback, useMemo, useState } from 'react';4
5function PageFixed() {6 const [theme, setTheme] = useState('light');7 const items = ['React', 'Vue', 'Angular', 'Svelte'];8
9 // useMemo stabilise la reference de l'objet config10 const config = useMemo(11 () => ({ pageSize: 20, showHeader: true }),12 [] // Pas de dependances = reference stable13 );14
15 // useCallback stabilise la reference de la fonction16 const handleItemClick = useCallback((item: string) => {17 console.log('Clic:', item);18 }, []);19
20 return (21 <div>22 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>23 Theme : {theme}24 </button>25
26 <ExpensiveList27 items={items}28 config={config} // Meme reference entre les renders29 onItemClick={handleItemClick} // Meme reference entre les renders30 />31 {/* Resultat : ExpensiveList ne se re-rend PAS32 quand le theme change */}33 </div>34 );35}Erreur 4 : useMemo pour des calculs triviaux
Utiliser useMemo pour des operations qui prennent quelques microsecondes (addition, concatenation, acces a une propriete) ajoute de la complexite sans benefice mesurable. Le cout de useMemo (comparaison des dependances, stockage en memoire) est du meme ordre que le calcul lui-meme.
1// ANTI-PATTERN : useMemo pour des calculs triviaux2
3function UserGreeting({ firstName, lastName, age }: UserProps) {4 // Inutile : la concatenation prend ~0.001ms5 const fullName = useMemo(6 () => `${firstName} ${lastName}`,7 [firstName, lastName]8 );9
10 // Inutile : une addition prend ~0.000001ms11 const birthYear = useMemo(12 () => new Date().getFullYear() - age,13 [age]14 );15
16 // Inutile : un acces a propriete prend ~0.0001ms17 const isAdult = useMemo(() => age >= 18, [age]);18
19 return (20 <div>21 <h2>{fullName}</h2>22 <p>Ne en {birthYear}</p>23 {isAdult && <span>Majeur</span>}24 </div>25 );26}27
28// CORRECTION : calcul direct, simple et lisible29
30function UserGreeting({ firstName, lastName, age }: UserProps) {31 const fullName = `${firstName} ${lastName}`;32 const birthYear = new Date().getFullYear() - age;33 const isAdult = age >= 18;34
35 return (36 <div>37 <h2>{fullName}</h2>38 <p>Ne en {birthYear}</p>39 {isAdult && <span>Majeur</span>}40 </div>41 );42}43
44// Regle : reservez useMemo aux calculs qui prennent > 1ms45// ou aux objets qui doivent avoir une reference stableResume des 4 erreurs a eviter
Gardez ces regles en tete pour utiliser la memoisation de maniere efficace et pertinente.
1. Tout memoiser par defaut
Chaque memoisation a un cout. Ne memoiser que ce que le Profiler identifie comme problematique.
2. Oublier des dependances
Les stale closures causent des bugs subtils. Activez le plugin ESLint exhaustive-deps.
3. memo sans stabiliser les props
React.memo est inutile si les objets et fonctions sont crees inline dans le parent.
4. useMemo pour des calculs triviaux
Reservez useMemo aux calculs > 1ms ou aux references qui doivent rester stables.
Dans quels cas la mémoisation est contre-productive ?
La memoisation n'est pas une optimisation gratuite. Elle a un cout en memoire, en complexite du code et en temps de comparaison des dependances. Dans de nombreux cas, ne PAS memoiser est la meilleure decision. Cette section etablit des regles concretes pour savoir quand la memoisation est contre-productive.
Regles de decision : faut-il memoiser ?
Avant d'ajouter React.memo, useMemo ou useCallback, passez par cette checklist.
1. Le composant est leger (leaf node)
Un composant qui rend quelques elements HTML sans enfants complexes se re-render en moins de 0.1ms. Le cout de React.memo (comparaison des props) est du meme ordre. Pas de memoisation.
2. Les props changent a chaque render
Si les props changent systematiquement (position dans une animation, timestamp, objet derive du state parent), React.memo compare et laisse passer a chaque fois. Vous payez le cout de comparaison sans aucun skip.
3. Pas de profiling prealable
Si vous n'avez pas mesure un probleme de performance avec le React DevTools Profiler, vous optimisez a l'aveugle. La memoisation prematuree ajoute de la complexite sans benefice prouve.
4. Le calcul est trivial
Les operations qui prennent moins de 1ms (concatenation, acces propriete, arithmetique simple) ne justifient pas useMemo. La comparaison des dependances coute autant que le calcul.
Exemple : quand la memoisation AJOUTE de la complexite
Voici un composant ou chaque memoisation est inutile. Le code est plus long, plus difficile a lire, et les performances sont identiques (voire legerement moins bonnes a cause de l'overhead).
1// ANTI-PATTERN : memoisation excessive sur un composant simple2
3import { memo, useMemo, useCallback, useState } from 'react';4
5// memo inutile : le composant est un leaf node trivial6const StatusBadge = memo(function StatusBadge({ active }: { active: boolean }) {7 return (8 <span className={active ? 'text-green-600' : 'text-red-600'}>9 {active ? 'Actif' : 'Inactif'}10 </span>11 );12});13
14function UserCard({ user }: { user: User }) {15 const [isEditing, setIsEditing] = useState(false);16
17 // useMemo inutile : concatenation instantanee18 const fullName = useMemo(19 () => `${user.firstName} ${user.lastName}`,20 [user.firstName, user.lastName]21 );22
23 // useMemo inutile : acces propriete24 const initials = useMemo(25 () => user.firstName[0] + user.lastName[0],26 [user.firstName, user.lastName]27 );28
29 // useCallback inutile : passe a un element natif <button>30 // Les elements natifs ne sont pas wrapes par React.memo31 const handleEdit = useCallback(() => {32 setIsEditing(true);33 }, []);34
35 // useCallback inutile : passe a un element natif <button>36 const handleCancel = useCallback(() => {37 setIsEditing(false);38 }, []);39
40 return (41 <div className="p-4 border rounded-lg">42 <div className="flex items-center gap-3">43 <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">44 {initials}45 </div>46 <div>47 <h3 className="font-medium">{fullName}</h3>48 <StatusBadge active={user.isActive} />49 </div>50 </div>51 <div className="mt-3">52 {isEditing ? (53 <button onClick={handleCancel}>Annuler</button>54 ) : (55 <button onClick={handleEdit}>Modifier</button>56 )}57 </div>58 </div>59 );60}1// VERSION CORRECTE : simple, lisible, performante2
3function StatusBadge({ active }: { active: boolean }) {4 return (5 <span className={active ? 'text-green-600' : 'text-red-600'}>6 {active ? 'Actif' : 'Inactif'}7 </span>8 );9}10
11function UserCard({ user }: { user: User }) {12 const [isEditing, setIsEditing] = useState(false);13
14 // Calculs directs : instantanes, pas besoin de cache15 const fullName = `${user.firstName} ${user.lastName}`;16 const initials = user.firstName[0] + user.lastName[0];17
18 return (19 <div className="p-4 border rounded-lg">20 <div className="flex items-center gap-3">21 <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">22 {initials}23 </div>24 <div>25 <h3 className="font-medium">{fullName}</h3>26 <StatusBadge active={user.isActive} />27 </div>28 </div>29 <div className="mt-3">30 {isEditing ? (31 <button onClick={() => setIsEditing(false)}>Annuler</button>32 ) : (33 <button onClick={() => setIsEditing(true)}>Modifier</button>34 )}35 </div>36 </div>37 );38}39
40// Resultat : meme performance, 30% de code en moins, plus lisibleLa regle d'or : mesurer d'abord, memoiser ensuite
Le React DevTools Profiler est l'outil qui distingue les optimisations utiles des optimisations prematurees. Il mesure le temps reel de chaque render et identifie les composants qui ralentissent votre application. Sans cette mesure, toute memoisation est une supposition.
1// Workflow de profiling pour decider de la memoisation2
3// Etape 1 : Installer les React DevTools (extension navigateur)4// Onglet "Profiler" dans les DevTools React5
6// Etape 2 : Enregistrer une session7// - Cliquer sur "Record"8// - Effectuer l'interaction a analyser (clic, saisie, navigation)9// - Cliquer sur "Stop"10
11// Etape 3 : Analyser les flamegraphs12// Le Profiler affiche :13// - Chaque composant qui a re-render14// - Le temps de render en millisecondes15// - La raison du re-render (props changed, state changed, hooks changed)16
17// Etape 4 : Identifier les candidats a la memoisation18// Chercher les composants qui :19// ✓ Se re-rendent frequemment (a chaque keystroke, scroll, etc.)20// ✓ Prennent > 1ms a re-render21// ✓ N'ont pas besoin de re-render (props inchangees)22
23// Etape 5 : Appliquer la memoisation ciblee24// UNIQUEMENT sur les composants identifies en etape 425
26// Exemple concret de profiling :27// Avant : <ProductList> re-render en 15ms a chaque keystroke du search28// Analyse : ProductList recoit une prop onSelect recree a chaque render29// Fix : useCallback sur onSelect + React.memo sur ProductList30// Apres : <ProductList> skip le re-render, search fluide31
32// Etape 6 : Re-profiler pour verifier33// Confirmer que la memoisation a bien reduit le temps de render34// Si pas d'amelioration mesurable : retirer la memoisation35
36// Le "Highlight updates" dans les React DevTools37// est aussi utile pour visualiser les re-renders en temps reel.38// Components > Settings > "Highlight updates when components render"39// Les composants qui re-rendent s'entourent d'une bordure coloree.Quand NE PAS memoiser : resume
Gardez ces situations en tete pour eviter la memoisation inutile.
Ne pas memoiser quand...
- -- Le composant est un leaf node simple
- -- Les props changent a chaque render
- -- Le calcul prend moins de 1ms
- -- Le callback est passe a un element natif
- -- Aucun profiling n'a ete fait
- -- Le composant enfant n'est pas wrape par React.memo
Memoiser quand...
- -- Le Profiler montre un re-render couteux (> 1ms)
- -- Le composant enfant est wrape par React.memo
- -- Le calcul traite > 100 elements
- -- La valeur est utilisee comme dependance de useEffect
- -- Le re-render cause un lag perceptible par l'utilisateur
- -- L'objet/fonction doit avoir une reference stable
Principe directeur
"Ecrivez d'abord du code lisible et simple. Mesurez les performances avec le Profiler. Memoiser uniquement la ou le Profiler montre un probleme. Re-mesurez pour confirmer le gain. Si le gain n'est pas mesurable, retirez la memoisation."
Le React Compiler va-t-il remplacer la mémoisation manuelle ?
Le React Compiler (anciennement React Forget) est un compilateur qui transforme automatiquement votre code React pour ajouter la memoisation optimale. Il analyse les dependances de chaque composant au build time et insere les equivalents de useMemo, useCallback et React.memo la ou c'est necessaire. L'objectif : ecrire du code React naturel et laisser le compilateur gerer les optimisations.
Avant/apres : le meme composant
Voici un composant tel que vous l'ecrivez, puis ce que le compilateur genere en sortie. Le code source reste simple et lisible, le compilateur gere l'optimisation.
1// CE QUE VOUS ECRIVEZ (code source)2
3function ProductPage({ products, category }: ProductPageProps) {4 const [search, setSearch] = useState('');5
6 // Calcul derive : filtre les produits7 const filteredProducts = products8 .filter(p => p.category === category)9 .filter(p => p.name.toLowerCase().includes(search.toLowerCase()));10
11 // Callback passe a un enfant12 const handleSelect = (id: string) => {13 router.push(`/products/${id}`);14 };15
16 return (17 <div>18 <SearchBar value={search} onChange={setSearch} />19 <ProductList20 products={filteredProducts}21 onSelect={handleSelect}22 />23 <ProductCount count={filteredProducts.length} />24 </div>25 );26}27
28// Aucun useMemo, aucun useCallback, aucun React.memo29// Le code est naturel, lisible et maintenable1// CE QUE LE COMPILATEUR GENERE (output simplifie)2
3function ProductPage({ products, category }: ProductPageProps) {4 const [search, setSearch] = useState('');5
6 // Le compilateur detecte que filteredProducts depend de7 // products, category et search.8 // Il insere l'equivalent d'un useMemo automatiquement.9 const filteredProducts = useMemo(10 () => products11 .filter(p => p.category === category)12 .filter(p => p.name.toLowerCase().includes(search.toLowerCase())),13 [products, category, search]14 );15
16 // Le compilateur detecte que handleSelect ne depend17 // d'aucune variable reactive (router est stable).18 // Il insere l'equivalent d'un useCallback.19 const handleSelect = useCallback(20 (id: string) => {21 router.push(`/products/${id}`);22 },23 [] // aucune dependance reactive24 );25
26 return (27 <div>28 <SearchBar value={search} onChange={setSearch} />29 <ProductList30 products={filteredProducts}31 onSelect={handleSelect}32 />33 <ProductCount count={filteredProducts.length} />34 </div>35 );36}37
38// Le compilateur a aussi wrape les composants enfants39// avec l'equivalent de React.memo la ou c'est pertinent.40// Tout cela est transparent : vous ne voyez que le code source.Ce que le compilateur fait vs ce qu'il ne fait pas
- Memoisation des valeurs derivees (equivalent useMemo)
- Memoisation des callbacks (equivalent useCallback)
- Memoisation des composants (equivalent React.memo)
- Analyse statique des dependances (jamais de stale closure)
- Granularite fine : memoisation au niveau expression, pas composant
- Compatible avec le code React existant sans modification
- •Tout projet React 19+ avec un build step
- •Migration progressive depuis un code avec memoisation manuelle
- Pas d'optimisation algorithmique (O(n^2) reste O(n^2))
- Pas de virtualisation (utiliser TanStack Virtual)
- Pas de code splitting (utiliser React.lazy)
- Pas de restructuration de composants (extraire les composants reste votre travail)
- Pas de gestion du state serveur (utiliser TanStack Query)
- Ne fonctionne pas avec des patterns non-standard (mutation directe, eval)
- •Cas ou le profiling montre un probleme non lie au re-render
- •Optimisations architecturales (lazy loading, virtualisation)
Ce que le compilateur FAIT | Ce que le compilateur NE FAIT PAS |
|---|---|
Optimisations automatiques inserees au build time par le React Compiler | Limites et cas ou l'intervention manuelle reste necessaire |
Avantages
| Avantages |
Inconvenients | Inconvenients
|
Cas d'usage
| Cas d'usage
|
Activer le React Compiler
1// Installation dans un projet Next.js 16+2// Le compilateur est inclus dans React 193
4// next.config.ts5import type { NextConfig } from 'next';6
7const nextConfig: NextConfig = {8 experimental: {9 reactCompiler: true,10 },11};12
13export default nextConfig;14
15// Pour un projet Vite / Webpack classique :16// npm install babel-plugin-react-compiler17
18// babel.config.js19module.exports = {20 plugins: [21 ['babel-plugin-react-compiler', {22 // Options du compilateur23 target: '19', // version React cible24 }],25 ],26};27
28// Verification : le compilateur log les composants optimises29// dans la console pendant le build.30// Si un composant ne peut pas etre optimise, un warning est emis.Les regles du compilateur
Le React Compiler applique les "Rules of React" de maniere stricte. Si votre code les enfreint, le compilateur ne peut pas optimiser le composant concerne. Voici les regles qui importent le plus pour la compatibilite.
1// REGLE 1 : Les composants et hooks doivent etre purs2// Le compilateur suppose que le rendu est deterministe3
4// COMPATIBLE : pas d'effet de bord pendant le render5function GoodComponent({ items }: { items: Item[] }) {6 const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));7 return <ul>{sorted.map(item => <li key={item.id}>{item.name}</li>)}</ul>;8}9
10// INCOMPATIBLE : mutation pendant le render11function BadComponent({ items }: { items: Item[] }) {12 items.sort((a, b) => a.name.localeCompare(b.name)); // Mutation du prop !13 return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;14}15
16
17// REGLE 2 : Les props et state sont immutables18// Toujours creer de nouveaux objets, jamais muter19
20// COMPATIBLE21function handleAdd(item: Item) {22 setItems(prev => [...prev, item]); // Nouveau tableau23}24
25// INCOMPATIBLE26function handleAddBad(item: Item) {27 items.push(item); // Mutation directe28 setItems(items); // Meme reference, React ne detecte pas le changement29}30
31
32// REGLE 3 : Les valeurs de retour et arguments des hooks sont immutables33// Ne pas muter ce que useState ou useReducer retourne34
35// COMPATIBLE36const [user, setUser] = useState({ name: 'Alice', age: 30 });37setUser({ ...user, age: 31 }); // Nouvel objet38
39// INCOMPATIBLE40user.age = 31; // Mutation directe de l'etat41setUser(user); // Meme reference42
43
44// REGLE 4 : Pas de lecture de valeurs pendant le render45// qui changent entre les renders sans etre des props/state46
47// COMPATIBLE48function Timer() {49 const [now, setNow] = useState(Date.now());50 useEffect(() => {51 const id = setInterval(() => setNow(Date.now()), 1000);52 return () => clearInterval(id);53 }, []);54 return <span>{new Date(now).toLocaleTimeString()}</span>;55}56
57// INCOMPATIBLE : lit Date.now() pendant le render58function BadTimer() {59 return <span>{new Date(Date.now()).toLocaleTimeString()}</span>;60 // Le compilateur ne peut pas savoir quand invalider le cache61}L'avenir de la memoisation React
Comment le paysage de l'optimisation React evolue avec le compilateur.
Aujourd'hui : transition progressive
Le React Compiler est stable et utilisable en production depuis React 19. La memoisation manuelle reste valide et coexiste avec le compilateur. Les projets existants peuvent migrer progressivement.
Court terme : moins de code manuel
Les nouveaux projets n'ont plus besoin d'ecrire useMemo/useCallback/React.memo. Le code source devient plus simple et plus lisible. Le plugin ESLint react-compiler aide a corriger les patterns incompatibles.
Long terme : memoisation manuelle obsolete
A mesure que l'ecosysteme adopte le compilateur, les hooks de memoisation manuelle deviendront progressivement inutiles. Comprendre le fonctionnement interne reste cependant essentiel pour diagnostiquer les problemes de performance.
Arbre de decision final
Voici la strategie recommandee pour gerer la memoisation dans vos projets React en 2025+.
1// ARBRE DE DECISION : memoisation React2
3// 1. Avez-vous active le React Compiler ?4// OUI → N'ecrivez aucune memoisation manuelle.5// Le compilateur gere tout automatiquement.6// Si un composant est lent, profilez et cherchez7// un probleme algorithmique, pas un probleme de memo.8//9// NON → Continuez vers l'etape 2.10
11// 2. Y a-t-il un probleme de performance mesure ?12// NON → N'ajoutez aucune memoisation. Revenez quand13// un utilisateur ou un profiling montre un probleme.14//15// OUI → Continuez vers l'etape 3.16
17// 3. Le probleme vient-il de re-renders inutiles ?18// (Verifier avec le React DevTools Profiler)19//20// NON → Le probleme est ailleurs (reseau, algorithme, bundle size).21// La memoisation ne resoudra rien.22//23// OUI → Continuez vers l'etape 4.24
25// 4. Quel type de re-render inutile ?26//27// a) Un composant enfant re-render car ses props n'ont pas change28// → React.memo sur l'enfant29// → useMemo/useCallback dans le parent pour stabiliser les props30//31// b) Un calcul couteux est reexecute inutilement32// → useMemo sur le calcul33//34// c) Une fonction recreee cause un re-render d'un enfant memoise35// → useCallback sur la fonction36
37// 5. Re-profilez apres la memoisation.38// Si pas d'amelioration mesurable → retirez la memoisation.39
40// RESUME EN UNE PHRASE :41// "Activez le compilateur. S'il n'est pas disponible,42// mesurez d'abord, memoisez ensuite, re-mesurez pour confirmer."Recapitulatif du guide
React.memo
HOC qui skip le re-render d'un composant si ses props n'ont pas change (shallow compare). Utile pour les composants couteux avec des props stables.
useMemo
Hook qui cache une valeur calculee. Utile pour les calculs couteux (> 1ms) et pour stabiliser les references d'objets passes a des enfants memoises.
useCallback
Hook qui cache une fonction. Equivalent a useMemo(() => fn, deps). Utile quand la fonction est passee a un enfant wrape par React.memo.
Avec le React Compiler, ces trois outils deviennent automatiques. Votre travail se concentre sur l'architecture, la lisibilite et les optimisations algorithmiques.
Felicitations !
Vous avez termine ce guide.