Retour au cours

frontend / react

Code splitting, lazy et Suspense

Leçon 141 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi un bundle JavaScript unique pénalise le temps de chargement initial
  • Déclarer un composant chargé à la demande avec lazy()
  • Afficher un état de chargement cohérent avec Suspense et son fallback
  • Découper le chargement par route pour ne charger que le code nécessaire à la page visitée
  • Précharger un module avant le clic pour masquer la latence perçue

Dans quel contexte ?

L'équipe remarque, via Lighthouse, que la page d'accueil src/pages/Accueil.jsx met plus de 4 secondes à devenir interactive sur mobile, alors qu'elle n'affiche qu'un formulaire de connexion. En creusant, le bundle JavaScript initial embarque aussi tout le code du tableau de bord et de l'éditeur de profil, jamais utilisés à ce stade. Découper ces pages avec lazy() et les charger uniquement quand l'utilisateur y navigue fait chuter ce temps de chargement de façon spectaculaire.

Étape 1 : le fonctionnement par défaut d'un build

Sans précaution particulière, un outil de build regroupe tout le code JavaScript d'une application en un seul fichier, téléchargé intégralement avant que la page ne devienne utilisable.

Étape 2 : le problème à grande échelle

Pour une application avec de nombreuses pages, un utilisateur qui n'a besoin que de la page d'accueil télécharge quand même tout le code du tableau de bord et des paramètres — un gaspillage évident.

Étape 3 : la solution, le code splitting

Le code splitting consiste à découper ce bundle unique en plusieurs morceaux, chargés séparément et seulement au moment où ils deviennent nécessaires.

Étape 4 : l'outil fourni par React

lazy() déclare qu'un composant doit être chargé de cette façon : le fichier qui le contient n'est réellement téléchargé que lors du premier rendu effectif de ce composant.

Étape 5 : pourquoi Suspense devient indispensable

Télécharger un morceau de code prend du temps, pendant lequel React ne peut pas encore afficher le composant demandé. Suspense définit ce qu'il faut montrer pendant cette attente.

Étape 6 : comment Suspense s'utilise

Il enveloppe la zone qui contient un ou plusieurs composants lazy, et affiche automatiquement son fallback tant que le chargement n'est pas terminé.

Prérequis

Cette leçon suppose une bonne compréhension de useEffect et du cycle de rendu : Suspense interrompt temporairement le rendu d'une branche de l'arbre, un mécanisme proche dans l'esprit mais différent dans le fonctionnement.

TechniqueQuand le code se téléchargeBénéfice principal
Bundle unique (par défaut)Tout au chargement initialSimplicité, mais lent au démarrage
lazy() par routeÀ la navigation vers la routeTemps de chargement initial réduit
Preload au survolAvant le clic, dès le survolLatence perçue quasi nulle au clic

Aller plus loin, anticiper le chargement

Un raffinement courant consiste à précharger un module avant même le clic, par exemple au survol d'un lien, ce qui masque une bonne partie de la latence perçue.

Et ensuite ?

Une fois le chargement à la demande maîtrisé, la prochaine étape explore un pattern d'architecture avancé : les compound components.

Commandes & code

Code splitting, lazy et Suspense

Charger le code JavaScript au moment où il devient nécessaire, pas tout d'un bloc au démarrage.

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

// lazy() : le module n'est téléchargé QUE lors du premier rendu réel du composant
const TableauDeBord = lazy(() => import("./TableauDeBord"));
const Parametres = lazy(() => import("./Parametres"));

function App() {
    const [page, setPage] = useState("dashboard");

    return (
        <div>
            <nav>
                <button onClick={() => setPage("dashboard")}>Dashboard</button>
                <button onClick={() => setPage("parametres")}>Paramètres</button>
            </nav>

            {/* Suspense affiche le fallback pendant le chargement du chunk JS */}
            <Suspense fallback={<p>Chargement du module...</p>}>
                {page === "dashboard" ? <TableauDeBord /> : <Parametres />}
            </Suspense>
        </div>
    );
}
jsx
// Code splitting par route (avec un routeur type React Router)
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";

const Accueil = lazy(() => import("./pages/Accueil"));
const Cours = lazy(() => import("./pages/Cours"));
const Profil = lazy(() => import("./pages/Profil"));

function App() {
    return (
        <Suspense fallback={<div className="spinner-page" />}>
            <Routes>
                <Route path="/" element={<Accueil />} />
                <Route path="/cours" element={<Cours />} />
                <Route path="/profil" element={<Profil />} />
            </Routes>
        </Suspense>
    );
}
// chaque route devient son propre "chunk" JS, téléchargé seulement quand l'utilisateur y navigue
jsx
// Preload manuel : anticiper le chargement AVANT le clic (ex: au survol d'un lien)
const ModaleCommentaires = lazy(() => import("./ModaleCommentaires"));

function BoutonCommentaires() {
    const [ouvert, setOuvert] = useState(false);

    function precharger() {
        import("./ModaleCommentaires"); // déclenche le téléchargement du chunk sans monter le composant
    }

    return (
        <>
            <button onMouseEnter={precharger} onClick={() => setOuvert(true)}>
                Voir les commentaires
            </button>
            {ouvert && (
                <Suspense fallback={<p>Chargement...</p>}>
                    <ModaleCommentaires />
                </Suspense>
            )}
        </>
    );
}
jsx
// Suspense imbriqué : granularité fine du fallback, zone par zone
function PageProduit({ id }) {
    return (
        <div>
            <EnTeteProduit id={id} /> {/* rendu immédiat, pas de lazy ici */}

            <Suspense fallback={<SqueletteAvis />}>
                <SectionAvis id={id} /> {/* peut avoir son propre fetch suspendu */}
            </Suspense>

            <Suspense fallback={<SquelettesRecommandations />}>
                <Recommandations id={id} />
            </Suspense>
        </div>
    );
}
// chaque section se révèle indépendamment dès qu'elle est prête,
// sans bloquer l'affichage des autres

Résumé

  • lazy() scinde le bundle : le code n'est téléchargé qu'au moment où le composant doit vraiment s'afficher.
  • Suspense doit englober tout arbre contenant un composant lazy, avec un fallback visuel cohérent.
  • Précharger au survol (import(...) sans montage) masque la latence réseau avant même le clic.
  • Plusieurs Suspense imbriqués permettent à chaque section de se révéler indépendamment des autres.

Exercices pratiques

1 disponible
1

Mission : ramener Accueil.jsx sous la seconde

Objectif : Découper un bundle unique avec lazy/Suspense et concevoir un préchargement au survol pour masquer la latence perçue.

Contexte

Lighthouse signale que Accueil.jsx, qui n'affiche qu'un formulaire de connexion, met plus de 4 secondes à devenir interactive sur mobile. En inspectant le bundle, TableauDeBord.jsx et ProfilEditeur.jsx — deux pages jamais visitées avant connexion — sont importés directement en haut du fichier App.jsx avec un import TableauDeBord from "./TableauDeBord"; classique, donc inclus dans le même chunk JavaScript que la page d'accueil.

Résoudre l’exercice →