Apogée Consult
Retour au blog
Mathieu Ponton
Mathieu PontonCo-Founder & ingénieur logiciel

Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.

Streaming SSR : ce que ça nous a vraiment apporté sur un site marketing à fort trafic

  • nextjs

On a activé le streaming SSR sur un site marketing qui recevait 40 000 visites/mois. Voici ce qu'on a mesuré, ce qui a changé, et ce qu'on n'avait pas anticipé.

Streaming SSR : ce que ça nous a vraiment apporté sur un site marketing à fort trafic

Le site en question : un site vitrine B2B avec une homepage lourde, hero animé, liste de témoignages chargés depuis un CMS, section blog avec les 3 derniers articles, widget de prix en temps réel. Environ 40 000 visites mensuelles, audience principalement mobile sur connexion 4G.

Notre LCP médian était à 3,4 secondes avant intervention. L'objectif était de passer sous 2,5 secondes sans refonte graphique.

Pourquoi le streaming, pas juste du SSG

La question s'est posée : pourquoi ne pas simplement statifier la page et revalider toutes les heures ?

Deux raisons nous en ont empêchés. D'abord, le widget de prix était dynamique par nature, il appelait une API tierce dont la réponse ne pouvait pas être mise en cache plus de 30 secondes sans créer de fausses attentes. Ensuite, les témoignages étaient éditoriaux et pouvaient changer plusieurs fois par jour, avec une exigence de publication immédiate.

On avait donc une page qui mélangeait du contenu quasi-statique (hero, navigation, blog) et du contenu dynamique (prix, témoignages récents). Le streaming SSR avec Suspense permettait de traiter les deux parties différemment dans un seul rendu.

L'architecture Suspense qu'on a adoptée

L'idée est simple : on envoie immédiatement le HTML pour tout ce qui est disponible, et on streame les parties dynamiques dès qu'elles sont prêtes.

// app/(marketing)/page.tsx
import { Suspense } from "react"

export default function HomePage() {
  return (
    <main>
      <HeroSection /> {/* statique, rendu immédiat */}
      <Suspense fallback={<TestimonialsSkeleton />}>
        <TestimonialsSection /> {/* fetch CMS, streamé */}
      </Suspense>
      <Suspense fallback={<PriceSkeleton />}>
        <PriceWidget /> {/* fetch API tierce, streamé */}
      </Suspense>
      <Suspense fallback={<BlogSkeleton />}>
        <LatestPosts /> {/* fetch CMS, streamé */}
      </Suspense>
    </main>
  )
}

Chaque composant sous Suspense est un Server Component async qui fetch ses données. Next.js commence à envoyer le HTML dès que la première partie est prête, sans attendre que tous les fetch soient terminés.

Ce qu'on a mesuré

Avant streaming, le TTFB médian était à 820ms. Le LCP médian à 3,4 secondes.

Après activation du streaming :

  • TTFB médian : 290ms (le HTML initial arrive beaucoup plus vite, seul le hero est attendu)
  • LCP médian : 2,1 secondes, sous l'objectif
  • First Contentful Paint médian : 1,4 secondes contre 2,6 avant

Les mesures viennent de CrUX sur une période de 28 jours. On a vérifié que le panel d'utilisateurs ne changeait pas significativement entre les deux périodes.

Le gain sur le LCP vient principalement du fait que le hero, qui contient l'image LCP, est rendu immédiatement sans attendre le fetch des témoignages. Avant, le serveur attendait que tous les fetch soient terminés avant d'envoyer la première ligne de HTML.

Ce qu'on n'avait pas anticipé

La gestion des skeletons devient un travail à part entière

Chaque fallback doit avoir les bonnes dimensions pour éviter les layout shifts à l'hydratation. Si le skeleton fait 200px et que le composant réel en fait 350px, on dégrade le CLS.

On a dû mesurer chaque section dynamique et construire des skeletons avec des hauteurs fixes en rem, calées sur la taille réelle du contenu. C'est du travail invisible mais indispensable.

L'ordre des blocs Suspense influe sur la priorité réseau

Les fetch dans les Server Components sous Suspense ne sont pas tous lancés en parallèle par défaut. Ils le sont si les composants sont au même niveau dans l'arbre. Mais si on imbrique des Suspense, les fetch s'enchaînent.

// Ces deux fetch se lancent en parallèle
<Suspense fallback={<A />}><ComponentA /></Suspense>
<Suspense fallback={<B />}><ComponentB /></Suspense>

// Attention : ComponentB attend ComponentA si imbriqués
<Suspense fallback={<A />}>
  <ComponentA>
    <Suspense fallback={<B />}><ComponentB /></Suspense>
  </ComponentA>
</Suspense>

On a eu un cas où l'imbrication était involontaire, un composant layout qui wrappait d'autres composants, et les fetch se retrouvaient séquentiels sans qu'on le comprenne. Résultat : le streaming annulait ses propres bénéfices.

Le streaming dégrade le cache CDN naïf

Un CDN configuré pour cacher les réponses complètes ne sait pas quoi faire avec un stream. On a dû ajuster la configuration Cloudflare pour ne pas cacher les routes qui utilisaient le streaming, ou configurer des règles de cache sur les segments statiques uniquement.

Sur un site à fort trafic, ce point est critique. Le streaming et le cache CDN se concilient, mais pas sans configuration explicite.

Pour quel type de page ça vaut vraiment le coup

Le streaming SSR est pertinent quand une page mélange contenu à disponibilité rapide et contenu à disponibilité lente. Une page homogène, tout statique ou tout dynamique, ne bénéficie pas du streaming de la même façon.

Sur ce projet, le ratio était environ 60% statique / 40% dynamique. C'est le cas idéal.

Pour une page 100% dynamique avec des données toutes aussi lentes à récupérer, le streaming ne change rien au temps total, on streame simplement plusieurs skeletons en même temps, ce qui n'améliore pas le LCP si l'élément LCP est dans la partie dynamique.

La question qu'on n'a pas encore résolue complètement : comment mesurer précisément l'impact du streaming sur le Core Web Vitals dans Lighthouse, qui ne tient pas compte du streaming de la même façon que CrUX ? Les scores Lighthouse et les métriques terrain peuvent diverger significativement sur ces architectures.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Next.js streaming SSR : résultats sur site marketing | Apogée Consult