🔍 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
📝 Exercices R11 ▶ Mini-projet : Benchmark 🧠 QCM R11 Suivant : Projet Final →