frontend / nextjs
Streaming et Suspense
Explication
Ce que vous allez apprendre
- Comprendre pourquoi une page peut rester blanche à cause d'une seule donnée lente
- Utiliser
loading.tsxpour un indicateur de chargement automatique au niveau d'un segment - Encadrer un composant serveur lent avec
<Suspense>pour un streaming granulaire - Consommer une Promise transmise depuis un Server Component avec
use()(React 19) - Éviter l'erreur classique qui annule tout l'intérêt du streaming (
awaittrop tôt)
Dans quel contexte ?
La page /produits/casque-audio d'une boutique en ligne affiche la fiche produit en 100 millisecondes, mais le bloc "recommandations personnalisées" en bas de page, calculé par un service de machine learning externe, met 3 secondes à répondre. Sans streaming, le visiteur regarde un écran blanc pendant ces 3 secondes entières alors que 99% du contenu était prêt depuis longtemps. Cette leçon montre comment afficher immédiatement ce qui est prêt et ne faire attendre que la partie réellement lente.
Le problème, d'abord
Sans streaming, une page attend que TOUTES ses données soient prêtes avant d'afficher quoi que ce soit. C'est une règle stricte, même si une seule partie de la page est lente.
Prenons un exemple concret. Un widget met 3 secondes à charger (une recommandation calculée par du machine learning) alors que le reste de la page ne prend que 100 millisecondes.
Résultat : l'utilisateur regarde un écran blanc pendant 3 secondes entières. Alors que 99% du contenu était prêt depuis longtemps et aurait pu s'afficher immédiatement.
L'idée du streaming corrige exactement ça. Le HTML est envoyé au navigateur PAR MORCEAUX, dès que chaque partie est prête, plutôt que d'attendre que tout soit fini.
Un premier outil simple existe déjà : loading.tsx. Il fournit un indicateur de chargement automatique au niveau d'un segment de route entier.
Mais l'outil le plus puissant est <Suspense>. En encadrant un composant serveur lent avec <Suspense fallback={...}>, on dit à Next.js : affiche immédiatement le reste de la page, et remplace ce fallback par le vrai contenu dès qu'il est prêt.
| Outil | Granularité | Déclenché par |
|---|---|---|
loading.tsx | Tout un segment de route | Chargement de page.tsx |
<Suspense> | Un composant précis | Une donnée lente dans ce composant |
use() | Une Promise transmise en props | Résolution de cette Promise côté client |
Prérequis
Cette leçon suppose que tu es à l'aise avec le data fetching serveur (await fetch dans un composant async) vu précédemment dans le cours.
Une fois qu'on sait streamer côté serveur, comment un Client Component reçoit-il une donnée qui arrive en retard ? React 19 introduit use(), qui permet de "consommer" une Promise transmise depuis un Server Component.
Une subtilité importante à ne pas rater ici. Le Server Component ne doit PAS faire await sur cette promesse avant de la transmettre — sinon on retombe dans le problème initial. Il la passe telle quelle, et use() combiné à <Suspense> gère l'attente au bon endroit.
Les pièges courants avant de pratiquer :
awaiter une donnée trop tôt dans le composant parent, ce qui annule tout l'intérêt du streaming pour cette donnée.- Oublier que
<Suspense>doit être un ANCÊTRE du composant qui utiliseuse(), sinon une erreur survient au runtime. - Croire que le streaming réduit le temps total de calcul serveur : ce n'est pas le cas, il améliore seulement la PERCEPTION de rapidité.
Piège fréquent
Faire const comments = await getComments(params) dans le composant serveur parent, puis passer comments (déjà résolu) au lieu de la Promise elle-même à un enfant encadré par <Suspense>, annule tout le bénéfice du streaming : le parent attend déjà la donnée lente avant de rendre quoi que ce soit.
Et la suite ? Cette leçon prolonge directement le Data Fetching et le Cache vus précédemment : une fois qu'on sait aller chercher des données, le streaming permet de ne plus faire attendre l'utilisateur pour la plus lente d'entre elles.
Commandes & code
Streaming et Suspense
Le streaming envoie le HTML au fur et à mesure qu'il est prêt, sans attendre la donnée la plus lente.
// app/dashboard/loading.tsx — affiché automatiquement pendant le chargement de page.tsx
export default function Loading() {
return <div className="spinner">Chargement du dashboard...</div>;
}// app/dashboard/page.tsx — Suspense granulaire : chaque widget streame indépendamment
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid">
{/* Rendu immédiat, ne dépend d'aucun fetch */}
<h1>Tableau de bord</h1>
<Suspense fallback={<CardSkeleton />}>
{/* Ce composant est lent : le reste de la page ne l'attend pas */}
<RevenueCard />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<OrdersCard />
</Suspense>
</div>
);
}
async function RevenueCard() {
const revenue = await fetch("https://api.example.com/revenue", {
cache: "no-store",
}).then((r) => r.json());
return <Card title="Revenus" value={revenue.total} />;
}
async function OrdersCard() {
const orders = await fetch("https://api.example.com/orders/count", {
cache: "no-store",
}).then((r) => r.json());
return <Card title="Commandes" value={orders.count} />;
}// Streaming avec plusieurs niveaux imbriqués — chaque partie s'affine indépendamment
import { Suspense } from "react";
export default function ProductPage({ params }: { params: Promise<{ id: string }> }) {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<ProductHeader params={params} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews params={params} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
{/* recommandations basées sur du ML : la partie la plus lente,
n'empêche pas le reste d'apparaître */}
<Recommendations params={params} />
</Suspense>
</div>
);
}// use() hook (React 19) — consommer une Promise dans un Client Component
"use client";
import { use } from "react";
interface Comment {
id: string;
text: string;
}
export default function Comments({
commentsPromise,
}: {
commentsPromise: Promise<Comment[]>;
}) {
// suspend le rendu jusqu'à résolution — doit être encapsulé dans un <Suspense> parent
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}// Composant serveur qui NE await PAS la promesse — la passe telle quelle pour streamer
export default function ArticlePage({ params }: { params: Promise<{ id: string }> }) {
const commentsPromise = getComments(params); // pas de "await" ici
return (
<article>
<ArticleBody />
<Suspense fallback={<p>Chargement des commentaires...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}Résumé
loading.tsxfournit un fallback automatique au niveau du segment de route.<Suspense>autour de composants serveur lents permet un streaming granulaire du HTML.- Ne pas
awaitune promesse trop tôt : la transmettre pour laisser React la streamer viause(). - Le streaming améliore le Time To First Byte perçu sans changer le temps total de traitement serveur.
Exercices pratiques
Mission : l'écran blanc de 3 secondes causé par les recommandations
Objectif : Isoler un composant lent avec Suspense et corriger un await qui annule le bénéfice du streaming.
Contexte
Sur app/produits/[id]/page.tsx, le code actuel fait const recommendations = await getRecommendations(id); directement dans le composant de page, AVANT de retourner le JSX contenant la fiche produit, les avis et le bloc recommandations. getRecommendations interroge un service de machine learning externe qui met 3 secondes à répondre, alors que le reste des données est prêt en 100 ms. Résultat : toute la page reste blanche pendant 3 secondes.