Tous les articles
·Ingénierie·7 min

Streaming et Suspense dans Next.js App Router : rendre l'attente invisible

Le streaming Next.js avec Suspense et loading.tsx envoie le shell HTML immédiatement et fait chuter le TTFB de 800 ms à moins de 100 ms sur du dynamique.

Une page qui attend la requête la plus lente avant d'afficher quoi que ce soit, c'est une page morte pendant 800 millisecondes. Le streaming Next.js règle ce problème à la racine : le serveur envoie le shell HTML dès qu'il est prêt, puis pousse le contenu dynamique par morceaux au fur et à mesure que les données arrivent. L'utilisateur voit une interface immédiatement, pas un écran blanc.

Sur l'App Router, ce comportement n'est pas une option à activer. Il est intégré au moteur de rendu React et se déclenche dès qu'une frontière Suspense entre en jeu. Le seul vrai travail, c'est de décider où placer ces frontières.

Cet article montre comment loading.tsx et Suspense découpent une page en unités qui se chargent indépendamment, ce que ça change mesurablement sur le TTFB et le LCP, et les pièges qui transforment un streaming bien pensé en cascade de requêtes lente.

Interface Next.js affichant un shell HTML pendant que le contenu streamé se charge
Le shell part immédiatement, le contenu dynamique arrive par chunks.

Le streaming Next.js, c'est quoi concrètement

Sans streaming, le serveur bloque. Il attend que toutes les requêtes de données soient résolues, assemble le HTML complet, puis l'envoie d'un bloc. Le TTFB est alors égal à la durée de la requête la plus lente. Si un appel base de données prend 700 ms, l'utilisateur attend 700 ms devant du vide.

Avec le streaming Next.js, la logique s'inverse. Le serveur rend d'abord la partie statique — layout, navigation, structure — et l'envoie sur le réseau. React produit ensuite le HTML restant en chunks alignés sur les frontières Suspense, et chaque morceau est injecté dans la page dès qu'il est prêt. Le TTFB tombe au temps nécessaire pour rendre les layouts et les fallbacks, souvent moins de 100 ms.

Techniquement, la réponse HTTP utilise un transfert chunked. Le navigateur reçoit un premier flux exploitable, affiche le shell, puis complète le DOM à chaque nouveau chunk. React gère aussi l'hydratation sélective : chaque frontière Suspense devient une unité d'hydratation indépendante, au lieu d'un unique passage bloquant sur toute la page.

loading.tsx : la frontière Suspense automatique

Next.js fournit un raccourci basé sur le système de fichiers. Un fichier loading.tsx placé dans un segment de route enveloppe automatiquement le page.tsx de ce segment dans une frontière Suspense. Aucune modification du composant page n'est nécessaire.

// app/dashboard/loading.tsx
// Rendu instantanément côté serveur pendant que page.tsx streame.
// Équivaut à <Suspense fallback={<DashboardSkeleton />}> sans toucher la page.
export default function Loading() {
  return <DashboardSkeleton />;
}

Le fichier est un Server Component par défaut, ce qui évite d'expédier du JavaScript client juste pour un squelette de chargement. Le fallback s'affiche pendant la navigation et pendant le rendu initial du segment. C'est le niveau de granularité le plus grossier : toute la page attend derrière un seul squelette.

Ce mécanisme couvre 80 % des besoins avec zéro effort. Un loading.tsx par segment lent suffit à supprimer les écrans blancs lors des navigations. Mais dès qu'une page mélange contenu rapide et contenu lent, ce niveau devient trop large et fait attendre inutilement des éléments déjà prêts.

Suspense manuel pour un streaming granulaire

La vraie puissance vient des frontières Suspense placées à la main. Quand plusieurs composants font du travail asynchrone — lecture en base, appel API — on enveloppe chacun dans sa propre frontière. Chaque morceau streame indépendamment, dans l'ordre où ses données arrivent, sans bloquer les autres.

// app/dashboard/page.tsx
// Chaque section a sa propre latence : on les isole pour qu'une
// requête lente ne retienne pas le contenu déjà disponible.
export default function Dashboard() {
  return (
    <>
      <Header />
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<FeedSkeleton />}>
        <ActivityFeed />
      </Suspense>
    </>
  );
}

Ici, Header fait partie du shell statique et s'affiche instantanément. Stats et ActivityFeed streament séparément : si le feed met 500 ms et les stats 80 ms, l'utilisateur voit les stats presque tout de suite. Cette approche transforme une attente perçue unique et longue en plusieurs apparitions progressives, ce qui est beaucoup plus supportable pour l'œil.

Le choix du placement n'est pas cosmétique. Une frontière trop haute annule le bénéfice ; une frontière par composant asynchrone donne le streaming le plus fluide. La règle pratique : une frontière autour de chaque source de données à latence significative.

Ce que le streaming Next.js change pour le TTFB et le LCP

Les chiffres sont nets. Les guides de performance récents rapportent un TTFB qui passe de 800 ms à moins de 100 ms sur des pages dynamiques une fois le shell découplé des données. Comme le streaming améliore le FCP, il pèse directement sur les Core Web Vitals et donc sur le référencement. Les applications qui adoptent pleinement les patterns Server Components rapportent des réductions de 50 à 70 % du JavaScript de premier chargement.

Mais il existe un piège de mesure sur le LCP. Si l'élément LCP — souvent une image ou un titre principal — se trouve à l'intérieur d'une frontière Suspense, il ne peut pas peindre tant que cette frontière n'est pas résolue. Le LCP se dégrade alors au lieu de s'améliorer.

Le bon réflexe : ce qui est critique pour la première impression reste dans le shell ; ce qui est secondaire et coûteux part en streaming. On optimise ainsi le LCP et le TTFB en même temps, sans les opposer.

Les pièges du streaming en production

Le premier piège est la cascade de requêtes. Si un composant parent attend sa donnée avant même de rendre l'enfant qui déclenche sa propre requête, les latences s'additionnent au lieu de se paralléliser. Il faut lancer les fetch au plus tôt et laisser Suspense orchestrer l'attente, pas enchaîner les await séquentiels.

Le deuxième concerne les codes de statut HTTP. Une fois le streaming commencé, les en-têtes de réponse sont déjà partis. Impossible de renvoyer un 404 ou un 500 propre après le premier chunk : la gestion d'erreur passe par error.tsx et les frontières d'erreur React, pas par le statut HTTP. C'est un changement de modèle mental à intégrer.

Dernier point : le streaming n'annule pas le cache. Les deux se combinent. Un segment mis en cache renvoie son shell encore plus vite, et seules les parties réellement dynamiques streament. Bien pensée, cette combinaison donne le meilleur des deux mondes.

En pratique

Le streaming Next.js n'est pas une micro-optimisation réservée aux gros projets. C'est le comportement par défaut de l'App Router, et l'ignorer revient à livrer volontairement des pages plus lentes qu'elles ne devraient l'être. La démarche tient en trois gestes : un loading.tsx sur chaque segment à latence, des frontières Suspense autour de chaque source de données lente, et l'élément LCP maintenu dans le shell statique.

Sur nos projets clients, ce découpage réduit systématiquement le temps perçu de chargement, même quand les requêtes back-end restent identiques. Le gain vient de la perception : montrer une structure et remplir progressivement bat toujours un écran figé. Le streaming se marie ensuite naturellement avec une stratégie de caching Next.js App Router pour accélérer encore le shell, et avec les nouveaux hooks React 19 pour gérer l'état côté client une fois le contenu arrivé.

Pour les pages riches en données, on combine souvent le streaming serveur avec une hydratation ciblée via TanStack Query v5, afin que les interactions restent fluides après le premier rendu. Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit de performance ou d'un renfort tech ? Parlons-en.

Sources

  1. Guides: Streaming — Next.js Documentation
  2. File-system conventions: loading.js — Next.js Documentation
  3. Core Web Vitals and Measurement — Vercel Academy