Retour au cours

frontend / nextjs

Performance avancée

Leçon 181 exercice

Explication

Ce que vous allez apprendre

  • Mesurer la composition du bundle JavaScript avec @next/bundle-analyzer
  • Retarder le chargement d'un composant lourd avec next/dynamic et ssr: false
  • Utiliser memo, useMemo et useCallback sans en abuser
  • Reconnaître un waterfall réseau et le corriger avec Promise.all ou preload
  • Éviter d'optimiser prématurément avant d'avoir mesuré le vrai goulot d'étranglement

Dans quel contexte ?

Une page /analytics affiche un graphique de revenus construit avec une librairie de visualisation de données assez lourde (plusieurs centaines de kilo-octets). Un audit de performance révèle que ce graphique est chargé dans le bundle JavaScript de TOUTES les pages de l'application, y compris celles qui ne l'affichent jamais, simplement parce qu'il est importé normalement en haut d'un fichier partagé. Cette leçon montre comment mesurer ce genre de problème et le corriger avec du chargement différé ciblé.

"Ça marche" n'est pas "c'est rapide"

Une application peut fonctionner parfaitement en développement, avec peu de composants et peu de données. Puis devenir lente en production une fois chargée de vraies données et de vraies librairies : graphiques, éditeurs de texte riches, lecteurs vidéo.

Cette leçon regroupe les techniques pour repérer et corriger ces ralentissements, une fois les bases du rendu et du data fetching maîtrisées.

Avant toute optimisation, il faut d'abord mesurer. @next/bundle-analyzer répond à une question essentielle : qu'est-ce qui rend mon JavaScript aussi lourd ?

Pourquoi cette étape ne peut pas être sautée ? Optimiser à l'aveugle, sans avoir mesuré, mène souvent à optimiser la mauvaise chose, en perdant du temps sur ce qui n'était pas le vrai problème.

Une fois qu'on sait ce qui pèse lourd, la première technique consiste à ne charger que ce qui est nécessaire. next/dynamic (et lazy de React) retardent le chargement d'un morceau de code jusqu'au moment où il est réellement utile.

Un exemple concret : un lecteur vidéo. Il ne se charge que lorsque l'utilisateur clique sur "voir la vidéo", plutôt qu'au chargement initial de toute la page.

Une option précise mérite d'être connue : ssr: false. Elle sert pour les librairies qui dépendent de l'objet window du navigateur, absent côté serveur.

Prérequis

Cette leçon suppose que tu es à l'aise avec la distinction Server/Client Component et le data fetching parallèle (Promise.all) vus précédemment : ici, on les combine avec des techniques de performance plus fines.

Une deuxième technique consiste à éviter le travail inutile côté React. memo, useMemo et useCallback existent pour ça, mais attention : ce ne sont pas des optimisations "magiques" à appliquer partout.

Utilisées mal à propos, elles peuvent même ralentir le code. Le coût de la mémoïsation dépasse parfois le coût du recalcul qu'elle était censée éviter.

Elles deviennent utiles dans deux cas précis : quand un calcul est réellement coûteux, ou quand elles empêchent un composant enfant mémoïsé de se re-render inutilement.

TechniqueRésoutRisque si mal utilisée
next/dynamicBundle trop lourd, chargé partoutFlash de contenu si mal géré (loading)
memoRe-render inutile d'un composant enfantCoût de comparaison si les props changent souvent
useMemo/useCallbackRecalcul ou re-création de référence coûteuxComplexité ajoutée sans gain mesurable
Promise.all/preloadRequêtes réseau en cascade (waterfall)Aucun si les requêtes sont vraiment indépendantes

Une troisième technique, déjà croisée dans la leçon sur le data fetching, revient ici sous un angle performance : éviter les cascades réseau. Démarrer plusieurs requêtes en parallèle plutôt qu'en série élimine des waterfalls invisibles à l'œil nu, mais qui ralentissent réellement le chargement perçu.

Le piège fréquent pour finir : sur-optimiser prématurément (mémoïser tout, code-splitter chaque petit composant) avant même d'avoir mesuré où se trouve le vrai goulot d'étranglement — ça ajoute de la complexité sans bénéfice réel mesurable.

Piège fréquent

Envelopper chaque composant de l'application dans memo() "par précaution" alourdit le code sans gain réel si ces composants sont petits ou re-render rarement. Mesure d'abord avec le profiler React DevTools, puis mémoïse UNIQUEMENT les composants identifiés comme coûteux.

Commandes & code

Performance avancée

bash
# Analyser la composition du bundle
npm install @next/bundle-analyzer
ANALYZE=true npm run build
js
// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer({
  experimental: {
    optimizePackageImports: ["lodash-es", "date-fns", "@mui/material"],
  },
});
tsx
// dynamic() — code splitting explicite pour du code lourd non critique
"use client";

import dynamic from "next/dynamic";

// Le chart.js/recharts n'est chargé QUE quand ce composant est monté
const RevenueChart = dynamic(() => import("@/components/RevenueChart"), {
  loading: () => <ChartSkeleton />,
  ssr: false, // désactive le rendu serveur si la lib dépend de "window"
});

export default function AnalyticsPage() {
  return (
    <div>
      <h1>Analytics</h1>
      <RevenueChart />
    </div>
  );
}
tsx
// Lazy loading d'un widget lourd affiché seulement sur interaction utilisateur
"use client";

import { useState, lazy, Suspense } from "react";

const VideoPlayer = lazy(() => import("@/components/VideoPlayer"));

export default function ProductVideo() {
  const [showVideo, setShowVideo] = useState(false);

  if (!showVideo) {
    return <button onClick={() => setShowVideo(true)}>Voir la vidéo de démo</button>;
  }

  return (
    <Suspense fallback={<p>Chargement du lecteur...</p>}>
      <VideoPlayer src="/demo.mp4" />
    </Suspense>
  );
}
tsx
// Éviter les re-renders inutiles : composants purs + mémoïsation ciblée
"use client";

import { memo, useMemo, useCallback, useState } from "react";

const ExpensiveList = memo(function ExpensiveList({
  items,
  onSelect,
}: {
  items: { id: string; name: string }[];
  onSelect: (id: string) => void;
}) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

export default function Catalog({ rawItems }: { rawItems: { id: string; name: string; archived: boolean }[] }) {
  const [selected, setSelected] = useState<string | null>(null);

  // recalculé seulement si "rawItems" change, pas à chaque re-render de Catalog
  const visibleItems = useMemo(() => rawItems.filter((i) => !i.archived), [rawItems]);

  // référence stable : évite de casser React.memo sur ExpensiveList
  const handleSelect = useCallback((id: string) => setSelected(id), []);

  return <ExpensiveList items={visibleItems} onSelect={handleSelect} />;
}
tsx
// Preload de données pour éviter un waterfall Client -> Server
// app/products/[id]/page.tsx
import { preload } from "react-dom";

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  preload(`/api/products/${id}/reviews`, { as: "fetch" }); // démarre le fetch tôt, en parallèle

  const product = await getProduct(id);
  return <ProductDetails product={product} />;
}

async function getProduct(id: string) {
  return { id, name: "Produit" };
}

Résumé

  • @next/bundle-analyzer révèle les dépendances qui gonflent le bundle client.
  • next/dynamic avec ssr: false retarde le chargement des libs lourdes non critiques au premier rendu.
  • memo/useCallback/useMemo ciblés évitent les re-renders en cascade sur de grosses listes.
  • Paralléliser les fetches (preload, Promise.all) plutôt que les enchaîner élimine les waterfalls réseau.

Exercices pratiques

1 disponible
1

Mission : le graphique de 400 Ko présent sur chaque page du site

Objectif : Isoler une librairie lourde du bundle global avec next/dynamic et éviter une mémoïsation appliquée sans avoir mesuré le vrai problème.

Contexte

@next/bundle-analyzer révèle que RevenueChart (qui importe une librairie de visualisation de 400 Ko) est présent dans le bundle JavaScript de TOUTES les pages du site, car il est importé normalement (import RevenueChart from "@/components/RevenueChart") tout en haut d'un fichier Layout partagé, alors que seule /analytics l'affiche réellement. Un développeur, en réaction à ce rapport, propose d'envelopper également TOUS les composants du site dans memo() "pour être sûr", sans avoir mesuré si un seul d'entre eux re-render réellement trop souvent.

Résoudre l’exercice →