frontend / react
Code splitting, lazy et Suspense
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
Suspenseet sonfallback - 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.
| Technique | Quand le code se télécharge | Bénéfice principal |
|---|---|---|
| Bundle unique (par défaut) | Tout au chargement initial | Simplicité, mais lent au démarrage |
lazy() par route | À la navigation vers la route | Temps de chargement initial réduit |
| Preload au survol | Avant le clic, dès le survol | Latence 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.
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>
);
}// 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// 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>
)}
</>
);
}// 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 autresRésumé
lazy()scinde le bundle : le code n'est téléchargé qu'au moment où le composant doit vraiment s'afficher.Suspensedoit englober tout arbre contenant un composantlazy, avec unfallbackvisuel cohérent.- Précharger au survol (
import(...)sans montage) masque la latence réseau avant même le clic. - Plusieurs
Suspenseimbriqués permettent à chaque section de se révéler indépendamment des autres.
Exercices pratiques
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.