Retour au cours

frontend / nextjs

Streaming et Suspense

Leçon 111 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi une page peut rester blanche à cause d'une seule donnée lente
  • Utiliser loading.tsx pour 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 (await trop 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.

OutilGranularitéDéclenché par
loading.tsxTout un segment de routeChargement de page.tsx
<Suspense>Un composant précisUne donnée lente dans ce composant
use()Une Promise transmise en propsRé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 utilise use(), 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.

tsx
// app/dashboard/loading.tsx — affiché automatiquement pendant le chargement de page.tsx
export default function Loading() {
  return <div className="spinner">Chargement du dashboard...</div>;
}
tsx
// 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} />;
}
tsx
// 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>
  );
}
tsx
// 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>
  );
}
tsx
// 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.tsx fournit un fallback automatique au niveau du segment de route.
  • <Suspense> autour de composants serveur lents permet un streaming granulaire du HTML.
  • Ne pas await une promesse trop tôt : la transmettre pour laisser React la streamer via use().
  • Le streaming améliore le Time To First Byte perçu sans changer le temps total de traitement serveur.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →