Retour au cours

frontend / react

Suspense avancé pour le data fetching

Leçon 271 exercice

Explication

Ce que vous allez apprendre

  • Identifier un waterfall de requêtes séquentielles involontaire dans un arbre de composants
  • Démarrer plusieurs promesses en parallèle avant le premier appel à use()
  • Coordonner l'ordre de révélation de plusieurs zones asynchrones avec SuspenseList
  • Dédupliquer un fetch appelé plusieurs fois dans le même rendu avec cache()
  • Comprendre le principe du streaming SSR pour afficher les sections prêtes en premier

Dans quel contexte ?

La page src/pages/PageProfil.jsx met presque 3 secondes à s'afficher entièrement, alors que chaque appel API individuel (/api/users/:id, /api/users/:id/posts) ne prend que 400ms. Le Profiler réseau montre que les deux requêtes partent l'une après l'autre plutôt qu'en parallèle : le composant SectionPosts ne démarre son fetch qu'après que PageProfil a fini de suspendre sur le sien. Réorganiser le code pour démarrer les deux promesses avant tout use() divise le temps de chargement total par deux.

Étape 1 : un piège facile à reproduire sans s'en rendre compte

Combiner use() et Suspense cache un piège : si un composant suspend sur une première requête AVANT de rendre un enfant qui en déclenche une seconde, celle-ci démarre en séquence.

Étape 2 : le phénomène du waterfall

On appelle ce phénomène un "waterfall" : chaque requête attend inutilement la précédente, alors que rien ne les empêchait réellement de partir en même temps.

Étape 3 : comment éviter cette cascade

La correction est simple une fois le problème identifié : démarrer TOUTES les promesses nécessaires avant le premier appel à use(), pour qu'elles partent réellement en parallèle.

Étape 4 : comment organiser ce code

Chaque promesse est ensuite transmise à un composant dédié, encapsulé dans son propre Suspense, qui se contente d'attendre une promesse déjà lancée.

Étape 5 : coordonner plusieurs zones asynchrones

Un autre problème apparaît quand plusieurs sections se chargent indépendamment : l'ordre d'apparition à l'écran peut devenir chaotique. SuspenseList impose un ordre de révélation cohérent.

Étape 6 : dédupliquer une même donnée

Il reste un dernier outil : cache() garantit qu'une seule requête réseau réelle est déclenchée quand deux composants distincts ont besoin de la même donnée dans le même rendu.

Piège fréquent

Appeler use(recupererPosts(userId)) directement dans un composant qui rend APRÈS un autre use() démarre la requête seulement une fois le premier terminé, même si rien ne les lie réellement. La règle : toujours lancer l'appel réseau (sans await/use()) avant de suspendre sur quoi que ce soit.

SymptômeCauseSolution
Chargement total = somme des temps de chaque requêteWaterfall involontaireDémarrer toutes les promesses avant le premier use()
Même requête déclenchée plusieurs fois dans un renduPas de déduplicationcache()
Sections qui apparaissent dans le désordrePlusieurs Suspense indépendantsSuspenseList

Et pour finir, le streaming SSR

Le streaming SSR permet au serveur d'envoyer le HTML des sections déjà prêtes en premier, sans attendre que tout soit résolu.

Et ensuite ?

Une fois ces techniques de data fetching maîtrisées, la prochaine étape aborde le rendu concurrent avec useTransition et useDeferredValue.

Commandes & code

Suspense avancé pour le data fetching

Éviter les waterfalls de requêtes séquentielles et orchestrer plusieurs zones asynchrones proprement.

jsx
// Waterfall involontaire : chaque fetch attend que le composant PARENT ait fini de rendre
function PageProfil({ userId }) {
    const utilisateur = use(recupererUtilisateur(userId)); // suspend ici...

    return (
        <div>
            <EnTete utilisateur={utilisateur} />
            <SectionPosts userId={userId} /> {/* ...son fetch ne démarre QU'APRÈS celui-ci */}
        </div>
    );
}

function SectionPosts({ userId }) {
    const posts = use(recupererPosts(userId)); // démarré en séquence, pas en parallèle -> lent
    return <ul>{posts.map((p) => <li key={p.id}>{p.titre}</li>)}</ul>;
}
jsx
// Correction : démarrer TOUTES les promesses AVANT de suspendre sur la première
function PageProfilCorrigee({ userId }) {
    // les deux fetch démarrent immédiatement, en parallèle -- avant tout "use()"
    const promesseUtilisateur = recupererUtilisateur(userId);
    const promessePosts = recupererPosts(userId);

    return (
        <div>
            <Suspense fallback={<SqueletteEntete />}>
                <EnTeteAsync promesse={promesseUtilisateur} />
            </Suspense>
            <Suspense fallback={<SqueletteListe />}>
                <SectionPostsAsync promesse={promessePosts} />
            </Suspense>
        </div>
    );
}

function EnTeteAsync({ promesse }) {
    const utilisateur = use(promesse); // ne fait qu'ATTENDRE une promesse déjà lancée
    return <h1>{utilisateur.nom}</h1>;
}
jsx
// SuspenseList : coordonne l'ORDRE de révélation de plusieurs Suspense frères (API encore expérimentale)
import { SuspenseList } from "react";

function Fil({ promessesPosts }) {
    return (
        <SuspenseList revealOrder="forwards" tail="collapsed">
            {/* forwards : révèle dans l'ordre, même si un post plus bas finit de charger avant
                tail="collapsed" : n'affiche qu'UN SEUL fallback à la fois pour le reste de la liste */}
            {promessesPosts.map((p, i) => (
                <Suspense key={i} fallback={<SquelettePost />}>
                    <Post promesse={p} />
                </Suspense>
            ))}
        </SuspenseList>
    );
}
jsx
// cache() : dédupliquer un fetch appelé plusieurs fois pendant le même rendu (Server Components / RSC)
import { cache } from "react";

const recupererUtilisateurCache = cache(async (id) => {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
});

// Deux composants différents appelant recupererUtilisateurCache(42) dans le même rendu
// ne déclenchent qu'UNE SEULE requête réseau -- le résultat est partagé
async function EnTete({ id }) {
    const utilisateur = await recupererUtilisateurCache(id);
    return <h1>{utilisateur.nom}</h1>;
}
async function CarteProfil({ id }) {
    const utilisateur = await recupererUtilisateurCache(id); // réutilise le résultat en cache
    return <p>{utilisateur.bio}</p>;
}
jsx
// Streaming SSR : le serveur envoie le HTML des sections prêtes en premier,
// puis complète les zones encore suspendues au fur et à mesure (renderToPipeableStream)
// -- le client n'attend pas que TOUT soit résolu pour voir une première réponse utile

Résumé

  • Démarrer toutes les promesses AVANT le premier use() évite les waterfalls séquentiels involontaires.
  • SuspenseList coordonne l'ordre de révélation de plusieurs Suspense frères, utile pour un fil d'actualité.
  • cache() déduplique un fetch appelé plusieurs fois pendant le même rendu (typiquement en Server Components).
  • Le streaming SSR envoie le HTML des sections prêtes en premier, sans attendre la résolution complète de la page.

Exercices pratiques

1 disponible
1

Mission : diviser par deux le temps de chargement de PageProfil

Objectif : Tracer précisément l'ordre d'exécution d'un waterfall involontaire, puis le corriger en démarrant les promesses en parallèle.

Contexte

PageProfil.jsx met presque 3 secondes à s'afficher, alors que /api/users/:id et /api/users/:id/posts prennent chacun 400ms côté serveur. Le code actuel suit exactement le pattern PageProfil de la leçon : const utilisateur = use(recupererUtilisateur(userId)); en tête du composant, suivi du rendu de <SectionPosts userId={userId} /> qui appelle use(recupererPosts(userId)) en interne.

Résoudre l’exercice →