🔍 Profiling React
Avant d'optimiser, mesurez ! Le DevTools React Profiler permet de voir quels composants se re-rendent et pourquoi.
// React.Profiler — mesurer les performances d'un sous-arbre
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration) {
console.log(`${id} (${phase}): ${actualDuration.toFixed(1)}ms`);
// id — nom du Profiler
// phase — "mount" ou "update"
// actualDuration — temps avec mémoïsation
// baseDuration — temps sans mémoïsation (référence)
}
function App() {
return (
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navbar />
</Profiler>
);
}
// Règles d'optimisation :
// 1. Ne pas optimiser prématurément (règle n°1)
// 2. Mesurer AVANT d'optimiser
// 3. useMemo/useCallback seulement si calcul vraiment coûteux
// 4. React.memo seulement si les re-renders sont fréquents et coûteux
La plupart des problèmes de performance React viennent de trop de re-renders, pas de calculs coûteux. Identifiez d'abord lequel avec le Profiler.
✂️ Code Splitting — React.lazy + Suspense
Divisez votre bundle JavaScript pour ne charger que ce qui est nécessaire au moment voulu.
import { lazy, Suspense } from 'react';
// Sans code splitting — tout est chargé au démarrage
// import Dashboard from './pages/Dashboard';
// import Settings from './pages/Settings';
// Avec React.lazy — chargement à la demande
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Charts = lazy(() => import('./components/Charts'));
function App() {
return (
// Suspense affiche un fallback pendant le chargement
<Suspense fallback={<div>Chargement...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Suspense boundary granulaire
function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<Charts /> {/* chargé seulement quand nécessaire */}
</Suspense>
</div>
);
}
📋 Virtualisation de listes
Pour les longues listes (1000+ items), ne rendre que les éléments visibles dans le viewport.
// Principe : ne rendre que les éléments visibles
// Bibliothèques populaires : react-window, react-virtual, @tanstack/virtual
// Avec @tanstack/virtual (recommandé)
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // hauteur estimée de chaque item
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() + 'px', position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start + 'px',
height: virtualItem.size + 'px',
width: '100%',
}}
>
{items[virtualItem.index].nom}
</div>
))}
</div>
</div>
);
}
// Solution manuelle simple pour de petits cas
function SimpleVirtualList({ items, itemHeight = 50, visibleCount = 10 }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const visible = items.slice(startIndex, startIndex + visibleCount);
return (
<div onScroll={e => setScrollTop(e.target.scrollTop)} style={{ height: visibleCount * itemHeight + 'px', overflow: 'auto' }}>
<div style={{ height: items.length * itemHeight + 'px', paddingTop: startIndex * itemHeight + 'px' }}>
{visible.map((item, i) => <div key={startIndex + i} style={{ height: itemHeight }}>{item.nom}</div>)}
</div>
</div>
);
}
🖼️ Images, Assets et optimisations diverses
// Lazy loading natif des images
function ProductCard({ image, alt }) {
return <img src={image} alt={alt} loading="lazy" decoding="async" />;
}
// Intersection Observer pour charger au scroll
function LazyImage({ src, alt }) {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setLoaded(true);
observer.disconnect();
}
});
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{loaded ? <img src={src} alt={alt} /> : <div className="skeleton" />}
</div>
);
}
// Éviter les re-renders avec des patterns stables
// ❌ Mauvais — nouvel objet à chaque render
function Parent() {
return <Child style={{ color: 'red' }} />; // nouvel objet à chaque render
}
// ✅ Bien — style défini en dehors du composant
const childStyle = { color: 'red' };
function Parent() {
return <Child style={childStyle} />; // même référence
}
// Batching automatique en React 18
// React 18 regroupe automatiquement plusieurs setState
function handleClick() {
setCount(c => c + 1); // React 18 : ces trois setState
setFlag(f => !f); // seront regroupés en un seul
setData(null); // re-render (automatic batching)
}
✅ Checklist Performance React
- ✅ Clés stables dans les listes — jamais d'index si l'ordre peut changer
- ✅ React.memo pour les composants qui reçoivent les mêmes props souvent
- ✅ useCallback pour les fonctions passées en props à des composants mémoïsés
- ✅ useMemo pour les calculs vraiment coûteux (> 1ms)
- ✅ Code splitting avec React.lazy pour les grosses pages
- ✅ Virtualisation pour les listes > 100 items
- ✅ loading="lazy" sur toutes les images hors viewport
- ✅ State local quand possible — éviter de tout mettre en Context
- ✅ Profiler pour identifier les bottlenecks avant d'optimiser
- ⚠️ Ne pas abuser de useMemo/useCallback — ils ont un coût overhead