
Tu sais ce qui fait plus mal qu'un bug en prod ? Une app qui marche... mais que personne veut utiliser parce qu'elle est trop lente.
C'est exactement ce qui m'est arrivé sur mon premier vrai projet.
Le code avait l'air correct. Ça marchait en développement. Mais en production, avec de vraies données et de vrais utilisateurs, tout semblait pataud. Les utilisateurs se plaignaient.
Ça m'a pris des semaines pour comprendre ce qui se passait. Voici les erreurs que j'ai faites et ce que j'ai appris en les corrigeant.
Erreur 1 : des re-renders partout
Je ne comprenais pas comment les re-renders React fonctionnent.
Chaque fois qu'un composant parent se re-render, tous ses enfants se re-renderent aussi. Même si leurs props n'ont pas changé. J'avais un dashboard avec 50+ composants, et cliquer sur un seul bouton déclenchait des centaines de re-renders.
function Dashboard() {
const [selectedTab, setSelectedTab] = useState('overview');
return (
<div>
<Tabs selected={selectedTab} onChange={setSelectedTab} />
<Header /> {/* Re-render à chaque changement d'onglet */}
<Sidebar /> {/* Re-render à chaque changement d'onglet */}
<Chart data={data} /> {/* Re-render à chaque changement d'onglet */}
<Table data={data} /> {/* Re-render à chaque changement d'onglet */}
<Footer /> {/* Re-render à chaque changement d'onglet */}
</div>
);
}
Chaque clic sur un onglet re-renderait tout. Le composant Chart était coûteux. La Table avait 500 lignes. Mes utilisateurs sentaient chaque re-render.
Le fix : React.memo
const Chart = memo(function Chart({ data }) {
// Ne re-render que si data change
return <ExpensiveChart data={data} />;
});
const Table = memo(function Table({ data }) {
// Ne re-render que si data change
return <ExpensiveTable data={data} />;
});
Maintenant cliquer sur les onglets ne re-render pas Chart ou Table parce que leur prop data ne change pas.
Mais il y a un piège. Je l'ai appris à mes dépens aussi.
Erreur 2 : casser memo avec de nouvelles références d'objets
J'ai wrappé tout dans memo. Problème résolu, non ?
Non.
function Dashboard() {
const [selectedTab, setSelectedTab] = useState('overview');
// Nouvel objet créé à chaque render !
const chartConfig = {
showLegend: true,
animate: false
};
return (
<Chart data={data} config={chartConfig} /> {/* Re-render quand même ! */}
);
}
memo fait une comparaison shallow. Chaque render crée un nouvel objet chartConfig. Nouvel objet = nouvelle référence = memo pense que les props ont changé = re-render.
Même problème avec les fonctions :
function Dashboard() {
// Nouvelle fonction créée à chaque render !
const handleClick = () => {
console.log('clicked');
};
return <Button onClick={handleClick} />; {/* Re-render quand même ! */}
}
Le fix : useMemo et useCallback
function Dashboard() {
const [selectedTab, setSelectedTab] = useState('overview');
// Même référence entre les renders
const chartConfig = useMemo(() => ({
showLegend: true,
animate: false
}), []);
// Même référence entre les renders
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<Chart data={data} config={chartConfig} />
<Button onClick={handleClick} />
</>
);
}
Maintenant memo marche vraiment parce que les références restent stables.
Erreur 3 : tout importer d'entrée
Mon bundle faisait 2.5MB.
Chaque utilisateur téléchargeait 2.5MB de JavaScript avant de voir quoi que ce soit. Le panneau admin ? Téléchargé. La page de paramètres ? Téléchargée. La fonctionnalité d'export rarement utilisée ? Téléchargée.
import { AdminPanel } from './AdminPanel';
import { Settings } from './Settings';
import { ExportFeature } from './ExportFeature';
import { Dashboard } from './Dashboard';
function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/settings" element={<Settings />} />
<Route path="/export" element={<ExportFeature />} />
</Routes>
);
}
Le fix : lazy loading
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const AdminPanel = lazy(() => import('./AdminPanel'));
const Settings = lazy(() => import('./Settings'));
const ExportFeature = lazy(() => import('./ExportFeature'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/settings" element={<Settings />} />
<Route path="/export" element={<ExportFeature />} />
</Routes>
</Suspense>
);
}
Maintenant les utilisateurs ne téléchargent que ce dont ils ont besoin. Le Dashboard charge en premier. Le panneau admin charge quand ils y naviguent.
Mon bundle initial est passé de 2.5MB à 400KB. Le first paint est passé de 4 secondes à moins d'1 seconde.
Erreur 4 : rendre des listes énormes
J'avais une table avec 5000 lignes. Je renderais les 5000.
function UserTable({ users }) {
return (
<table>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
);
}
5000 noeuds DOM. Le scroll était un diaporama.
Le fix : virtualisation
import { FixedSizeList } from 'react-window';
function UserTable({ users }) {
const Row = ({ index, style }) => (
<div style={style}>
{users[index].name} - {users[index].email}
</div>
);
return (
<FixedSizeList
height={500}
itemCount={users.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Maintenant seules les lignes visibles sont dans le DOM. 5000 items ? Toujours seulement ~20 noeuds DOM à la fois. Le scroll est fluide comme du beurre.
Librairies qui font ça : react-window, react-virtualized, @tanstack/react-virtual.
Erreur 5 : ne pas utiliser le React DevTools Profiler
Je devinais où étaient les problèmes de performance. Parfois je devinais juste. La plupart du temps non.
Puis j'ai découvert le React DevTools Profiler.
Il te montre :
- Quels composants ont rendu
- Combien de temps chaque render a pris
- Ce qui a déclenché le render
- Les renders "gaspillés" (renders qui n'ont produit aucun changement visible)
Comment l'utiliser
- Installe l'extension React DevTools
- Ouvre DevTools → onglet Profiler
- Clique sur "Record"
- Fais l'action lente dans ton app
- Clique sur "Stop"
- Vois exactement quels composants sont lents
Le Profiler m'a montré que mon composant Table prenait 800ms à render. Pas parce qu'il était complexe, mais parce qu'il re-renderait à chaque frappe dans un input de recherche.
Je n'aurais jamais trouvé ça en lisant le code.
Erreur 6 : calculs coûteux dans le render
Je filtrais et triais les données dans le render :
function UserList({ users, searchTerm, sortBy }) {
// S'exécute à CHAQUE render
const filteredUsers = users
.filter(u => u.name.includes(searchTerm))
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
return (
<ul>
{filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Avec 5000 utilisateurs, filtrer et trier à chaque render tuait les performances.
Le fix : useMemo
function UserList({ users, searchTerm, sortBy }) {
const filteredUsers = useMemo(() => {
return users
.filter(u => u.name.includes(searchTerm))
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
}, [users, searchTerm, sortBy]);
return (
<ul>
{filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Maintenant le calcul ne s'exécute que quand users, searchTerm, ou sortBy changent vraiment. Pas à chaque re-render du parent.
Erreur 7 : utiliser index comme key
Celle-là m'a bien mordu.
{items.map((item, index) => (
<Item key={index} data={item} />
))}
Ça a l'air correct. Ça marche bien... jusqu'à ce que tu ajoutes, supprimes, ou réordonnes des items.
React utilise les keys pour tracker quels items ont changé. Si tu utilises index comme key :
- Supprime item 0 → React pense que item 0 est devenu item 1, item 1 est devenu item 2...
- Chaque item re-render avec les mauvaises données
- Le state se mélange entre les items
Le fix : utiliser des IDs uniques
{items.map(item => (
<Item key={item.id} data={item} />
))}
Utilise toujours un identifiant stable et unique. Si tes données n'ont pas d'IDs, crée-les quand les données sont créées, pas pendant le render.
Le modèle mental que j'aurais aimé avoir
La cheat sheet
| Problème | Symptôme | Fix |
|---|---|---|
| Re-renders inutiles | Cliquer cause du lag | React.memo |
| Nouvelles refs objet/fonction | memo n'aide pas | useMemo / useCallback |
| Gros bundle | Chargement initial lent | React.lazy + code splitting |
| Longues listes | Lag au scroll | react-window virtualisation |
| Calculs coûteux | Lag sur interactions | useMemo |
| Mauvaises keys | Items de liste mélangés | Utiliser des IDs uniques, pas index |
La leçon
La performance c'est pas de la magie. C'est mesure + compréhension.
J'ai passé des semaines à deviner. J'aurais pu passer des heures à mesurer.
Le React DevTools Profiler m'aurait montré exactement où étaient les problèmes. La doc m'aurait appris memo, useMemo, et le lazy loading. Mais je pensais pouvoir comprendre tout seul.
La méthode difficile m'a appris : mesure d'abord, optimise ensuite, devine jamais.
C'est la partie 2 de ma série "Ce que j'ai appris à mes dépens". Prochain article : comment je structure un projet Next.js après 3 ans.
Continuer vers la partie 3 : Comment je structure un projet Next.js après 3 ans →
Des questions ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.