frontend / react
Suspense avancé pour le data fetching
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ôme | Cause | Solution |
|---|---|---|
| Chargement total = somme des temps de chaque requête | Waterfall involontaire | Démarrer toutes les promesses avant le premier use() |
| Même requête déclenchée plusieurs fois dans un rendu | Pas de déduplication | cache() |
| Sections qui apparaissent dans le désordre | Plusieurs Suspense indépendants | SuspenseList |
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.
// 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>;
}// 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>;
}// 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>
);
}// 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>;
}// 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 utileRésumé
- Démarrer toutes les promesses AVANT le premier
use()évite les waterfalls séquentiels involontaires. SuspenseListcoordonne l'ordre de révélation de plusieursSuspensefrè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
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.