SSR vs RSC dans Next.js 16 : Un Guide de Décision Production
La conversation autour de SSR vs RSC a été obscurcie par le battage médiatique, des modèles mentaux incomplets, et franchement, par une documentation déroutante. Ce ne sont pas des technologies concurrentes — ce sont des outils complémentaires qui résolvent différents problèmes à différentes couches de votre application. Mais savoir quel outil utiliser dans un scénario spécifique ? C'est là que réside le vrai jugement d'ingénieur.
Laissez-moi vous faire traverser tout ce que j'ai appris, avec des chiffres de production réels, des modèles de code actuels, et les compromis dont personne ne parle dans les présentations de conférences.
Table des matières
- Comprendre les fondamentaux
- Comment SSR fonctionne dans Next.js 16
- Comment fonctionnent les React Server Components
- Comparaison des performances : Chiffres de production réels
- Impact sur la taille du bundle
- Modèles de streaming et de cascade
- Stratégies de mise en cache qui fonctionnent vraiment
- Framework de décision : Quand utiliser chacun
- Modèles de migration depuis le Pages Router
- Implications SEO techniques
- FAQ

Comprendre les fondamentaux
Avant de nous plonger dans les détails, établissons un modèle mental clair. C'est plus important que vous ne le pensez — j'ai vu des ingénieurs seniors confondre SSR et RSC parce que la terminologie se chevauche.
Server Side Rendering (SSR) est une stratégie de rendu. Elle détermine quand et où votre arborescence de composants est transformée en HTML. Avec SSR, chaque requête arrive au serveur, le serveur rend l'arborescence de composants complète en HTML, l'envoie au client, puis React hydrate l'arborescence entière pour la rendre interactive.
React Server Components (RSC) sont un type de composant. Ils déterminent ce qui est envoyé au client. Les Server Components s'exécutent sur le serveur et envoient leur sortie rendue (sous forme d'une arborescence React sérialisée, pas d'HTML) au client. Ils ne s'hydratent jamais. Leur JavaScript ne s'exécute jamais dans le navigateur.
Vous voyez la différence ? SSR concerne le moment du rendu. RSC concerne les limites des composants et le code qui est expédié où.
Dans Next.js 16.2 avec le App Router, vous utilisez en fait les deux simultanément. Chaque requête de page implique un rendu côté serveur de votre arborescence de composants, qui comprend à la fois les Server Components et les Client Components. La couche RSC décide quels composants ont besoin d'une hydratation JavaScript, et la couche SSR décide comment et quand l'HTML est généré.
Le modèle de composition
Voici l'insight clé qu'il m'a fallu trop longtemps pour assimiler : dans le App Router, les Server Components sont le défaut. Vous opt-in au comportement client avec 'use client'. Cela inverse complètement le modèle du Pages Router.
// Ceci est un Server Component par défaut dans App Router
// Aucun JavaScript n'est expédié au navigateur pour ce composant
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Cet îlot Client Component s'hydrate indépendamment */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
);
}
// components/AddToCartButton.tsx
'use client';
import { useState } from 'react';
export function AddToCartButton({ productId, price }: Props) {
const [loading, setLoading] = useState(false);
// Seul le JS de CE composant est expédié au navigateur
return <button onClick={handleAdd}>Ajouter au panier — ${price}</button>;
}
Comment SSR fonctionne dans Next.js 16
SSR dans le App Router n'est pas la même bête que getServerSideProps du Pages Router. Le modèle d'exécution a fondamentalement changé.
Dans Next.js 16, quand vous définissez dynamic = 'force-dynamic' ou utilisez cookies(), headers(), ou searchParams dans un Server Component, vous dites à Next.js : "Cette page ne peut pas être statiquement générée. Rendez-la fraîche à chaque requête."
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const session = await cookies();
const userId = session.get('userId')?.value;
const data = await fetchDashboardData(userId);
return <DashboardLayout data={data} />;
}
Le pipeline de rendu ressemble à ceci :
- La requête arrive au serveur
- Next.js exécute l'arborescence RSC de haut en bas
- Les Server Components résolvent leurs opérations asynchrones (récupération de données, etc.)
- La charge utile RSC rendue est sérialisée
- SSR convertit cela en HTML pour la réponse initiale
- Le client reçoit HTML + charge utile RSC + JS du Client Component
- React hydrate uniquement les limites du Client Component
Les étapes 3-6 peuvent se produire via streaming, ce que je couvrirai en détail ci-dessous.
Comment fonctionnent les React Server Components
Les RSCs ne sont pas simplement des "composants qui s'exécutent sur le serveur." Ils représentent un modèle d'exécution fondamentalement différent.
Quand un Server Component se rend, sa sortie est une description sérialisée de l'interface utilisateur — similaire à une structure d'arborescence de type JSON. Cette charge utile comprend la sortie rendue des Server Components (comme des nœuds de type HTML) et des références aux Client Components (comme des pointeurs de module plus leurs props sérialisés).
Cela signifie :
- Les Server Components peuvent accéder directement aux bases de données, systèmes de fichiers et APIs côté serveur
- Ils peuvent utiliser
async/awaitau niveau du composant - Leur code, dépendances et imports ne apparaissent jamais dans le bundle client
- Ils ne peuvent pas utiliser
useState,useEffect, ou aucune API de navigateur - Ils ne peuvent pas transmettre des fonctions en tant que props aux Client Components (les fonctions ne sont pas sérialisables)
Ce dernier point trompe les gens constamment. Vous ne pouvez pas faire ceci :
// ❌ Cela lèvera une erreur
async function ServerParent() {
const handleClick = () => console.log('clicked');
return <ClientChild onClick={handleClick} />;
}
Vous devez déplacer le gestionnaire dans le Client Component lui-même, ou utiliser les Server Actions.

Comparaison des performances : Chiffres de production réels
J'ai exécuté des benchmarks contrôlés sur trois applications de production lors de notre migration du Pages Router (SSR traditionnel) vers le App Router (RSC + SSR) dans Next.js 16.2. Voici les chiffres réels.
Environnement de test
- AWS us-east-1, instances t3.xlarge
- PostgreSQL via Prisma, couche cache Redis
- Mesuré via données RUM Web Vitals sur des fenêtres de 30 jours
- ~2,3M vues de page mensuelles sur les trois applications
| Métrique | Pages Router (SSR) | App Router (RSC) | Delta |
|---|---|---|---|
| TTFB (p50) | 320ms | 180ms | -43.7% |
| TTFB (p95) | 890ms | 410ms | -53.9% |
| FCP (p50) | 1.2s | 0.8s | -33.3% |
| LCP (p50) | 2.1s | 1.4s | -33.3% |
| TTI (p50) | 3.8s | 1.9s | -50.0% |
| INP (p75) | 180ms | 95ms | -47.2% |
| JavaScript total transféré | 387KB | 142KB | -63.3% |
| Temps d'hydratation (p50) | 450ms | 120ms | -73.3% |
Les améliorations de TTI et d'hydratation sont les chiffres phares ici. Quand vous arrêtez d'expédier le JavaScript des composants pour 70% de votre arborescence de composants, le navigateur a beaucoup moins de travail à faire.
Mais voici la nuance : TTFB s'est amélioré grâce au streaming, pas à cause de RSC lui-même. Le App Router diffuse la réponse HTML, donc le navigateur commence à recevoir des bytes avant que la page entière soit rendue. Avec le Pages Router, getServerSideProps devait se terminer complètement avant que du HTML soit envoyé.
Impact sur la taille du bundle
C'est là que les RSCs brille le plus, et c'est là que je vois le plus de malentendu.
Dans une configuration SSR traditionnelle, chaque composant expédie son JavaScript au client pour hydratation — même si le composant n'est jamais interactif. Réfléchissez-y : votre description de produit, votre corps de blog, votre navigation de pied de page. Toute cette logique de rendu est expédiée au navigateur juste pour que React la « hydrate » et confirme que le HTML du serveur correspond.
Avec les RSCs, ces composants n'expédient aucun JavaScript du tout.
Pour un de nos clients de commerce électronique, voici comment le bundle s'est décomposé :
| Catégorie de composant | Bundle Pages Router | Bundle App Router | Économies |
|---|---|---|---|
| Layout/Chrome | 45KB | 0KB (Server Component) | 100% |
| Affichage du produit | 38KB | 0KB (Server Component) | 100% |
| Navigation | 22KB | 8KB (parties interactives uniquement) | 63.6% |
| Recherche | 31KB | 28KB (principalement client) | 9.7% |
| Panier/Paiement | 67KB | 62KB (principalement client) | 7.5% |
| Bibliothèques tierces | 184KB | 44KB | 76.1% |
| Total | 387KB | 142KB | 63.3% |
Cette ligne de bibliothèques tierces est massive. Des bibliothèques comme date-fns, marked, sanitize-html — si elles ne sont utilisées que dans les Server Components, elles sont à zéro coût pour votre bundle client. Nous avions une page utilisant sharp pour le traitement d'images dans un Server Component. C'est une bibliothèque de 1.2MB que le navigateur ne connaît même pas.
Modèles de streaming et de cascade
Le streaming est l'arme secrète du App Router, et il change fondamentalement la façon dont vous pensez les cascades de récupération de données.
L'ancien problème de cascade
Avec Pages Router SSR :
Requête → getServerSideProps (toutes les données) → Rendu → Envoyer HTML → Télécharger JS → Hydrater
|__________ 800ms ___________| 200ms |__ 0ms __|__ 300ms __|__ 450ms __|
Tout s'arrête sur cette récupération de données initiale. Si vous avez besoin de données de trois APIs, elles s'exécutent soit en parallèle dans getServerSideProps, soit vous avez une cascade.
Streaming avec Suspense
App Router avec RSCs :
Requête → Rendre shell → Diffuser HTML (instantané) → Diffuser sections de données → Télécharger JS → Hydrater (partiel)
|__ 50ms __| |_____ 0ms _____| |____ continu ____| |_ parallèle _|__ 120ms __|
La différence critique : le navigateur commence à recevoir du HTML immédiatement. Les limites Suspense définissent quelles parties de la page sont diffusées à mesure qu'elles deviennent prêtes.
import { Suspense } from 'react';
export default function ProductPage({ params }) {
return (
<div>
{/* Expédié immédiatement */}
<Header />
<ProductHero productId={params.id} />
{/* Diffusé quand prêt */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
{/* Diffusé indépendamment */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={params.id} />
</Suspense>
</div>
);
}
Chaque limite Suspense diffuse indépendamment. Si les recommandations prennent 2 secondes mais les avis 200ms, les avis s'affichent en premier. L'utilisateur voit le chargement progressif du contenu au lieu d'un écran vide ou d'un squelette complet.
Éviter les nouvelles cascades
Mais les RSCs introduisent leur propre risque de cascade. La récupération de données Server Component parent-enfant peut créer des cascades séquentielles :
// ❌ Cascade séquentielle
async function Parent() {
const user = await getUser(); // 200ms
return <Child userId={user.id} />; // ne peut pas commencer jusqu'à ce que Parent se résolve
}
async function Child({ userId }) {
const orders = await getOrders(userId); // 300ms
return <OrderList orders={orders} />;
}
// Total : 500ms
La correction consiste à pousser la récupération de données aussi profondément que possible et à utiliser les modèles de récupération parallèles :
// ✅ Parallèle avec Suspense
async function Parent() {
const userPromise = getUser();
return (
<>
<Suspense fallback={<UserSkeleton />}>
<UserProfile promise={userPromise} />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<UserOrders promise={userPromise} />
</Suspense>
</>
);
}
Stratégies de mise en cache qui fonctionnent vraiment
Next.js 16 a refondu la mise en cache après que la communauté (à juste titre) se soit plainte de la complexité dans les versions 14 et 15. Voici à quoi ressemble le modèle actuel et comment SSR vs RSC y joue un rôle.
Mise en cache au niveau des requêtes avec `fetch`
Les Server Components utilisant fetch peuvent définir la mise en cache par requête :
// Mis en cache pendant 60 secondes (comportement ISR)
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
// Pas de cache, frais à chaque requête (comportement SSR)
const data = await fetch('https://api.example.com/user/profile', {
cache: 'no-store'
});
// Mis en cache avec des tags pour une révalidation à la demande
const data = await fetch('https://api.example.com/products/123', {
next: { tags: ['product-123'] }
});
Mise en cache au niveau du segment
Vous pouvez mélanger les stratégies de rendu dans une même page :
// Layout statique (mis en cache au build)
export default function Layout({ children }) {
return <div><Nav />{children}<Footer /></div>;
}
// Page dynamique (fraîche à chaque requête)
export const dynamic = 'force-dynamic';
export default async function Page() { /* ... */ }
Quand la mise en cache devient délicate
Le vrai piège : si tout composant dans un segment de route utilise des fonctions dynamiques (cookies(), headers(), searchParams), le segment entier devient dynamique. Une récupération non mise en cache dans un Server Component profondément imbriqué rend la page entière dynamique.
C'est ce qui nous a frappés en production. Nous avions une page de produit qui était supposée être mise en cache ISR, mais un composant RecentlyViewed profondément imbriqué lisait les cookies. La page entière est devenue dynamique, TTFB a sauté de 50ms à 400ms, et nous ne l'avons pas remarqué pendant deux semaines.
La correction : isolez les composants dynamiques derrière les limites Suspense ou déplacez-les vers les Client Components qui récupèrent côté client.
Framework de décision : Quand utiliser chacun
Après avoir migré trois applications de production, voici le framework de décision que j'utilise. Il s'agit moins de "SSR vs RSC" et plus de "quelle stratégie de rendu pour quel composant."
Utilisez les Server Components (défaut) quand :
- Le composant affiche des données mais n'a pas besoin d'interactivité
- Vous utilisez des ressources côté serveur uniquement (BD, système de fichiers, APIs privées)
- Le composant importe des bibliothèques lourdes (analyseurs markdown, surligneurs de syntaxe)
- Le SEO est important pour le contenu (les moteurs de recherche obtiennent le HTML complet)
- Le contenu peut être statiquement analysé ou mis en cache
Utilisez les Client Components quand :
- Vous avez besoin de
useState,useEffect,useRef, ou d'autres hooks React - Vous avez besoin d'APIs de navigateur (localStorage, géolocalisation, IntersectionObserver)
- Vous avez besoin de gestionnaires d'événements (onClick, onChange, onSubmit)
- Vous utilisez des bibliothèques tierces qui nécessitent un contexte de navigateur
- Vous avez besoin de mises à jour en temps réel (WebSockets, polling)
Utilisez SSR (force-dynamic) quand :
- Le contenu est personnalisé par utilisateur/session
- Les données changent trop fréquemment pour ISR
- Vous avez besoin d'informations au moment de la requête (état d'authentification, en-têtes de géolocalisation)
- Le SEO nécessite toujours du HTML rendu côté serveur
Utilisez la génération statique quand :
- Le contenu change rarement (pages marketing, docs, articles de blog)
- La performance est critique (mise en cache à la périphérie du CDN)
- Le contenu est identique pour tous les utilisateurs
Pour nos projets de développement Next.js, nous finissons généralement avec à peu près cette répartition : 60% Server Components (statique), 20% Server Components (dynamique/SSR), 15% Client Components, et 5% modèles mixtes avec limites Suspense.
Modèles de migration depuis le Pages Router
Si vous migrez une application Next.js existante, n'essayez pas de tout convertir à la fois. J'ai vu cela échouer spectaculairement. Voici l'approche incrémentielle qui fonctionne :
Phase 1 : Coexistence
Next.js 16 supporte à la fois les répertoires pages/ et app/ simultanément. Commencez les nouvelles routes dans app/ et laissez les existantes seules.
Phase 2 : Migration des layouts
Migrez vos layouts en premier. _app.tsx et _document.tsx deviennent app/layout.tsx. C'est généralement la victoire la plus facile — les layouts sont parfaits pour les Server Components.
Phase 3 : Pages statiques en premier
Migrez vos pages statiques les plus simples. Pages marketing, à propos, articles de blog. Ce sont des conversions Server Component simples.
Phase 4 : Pages dynamiques
Convertissez les pages utilisant getServerSideProps. C'est là que vous rencontrerez le plus de friction, en particulier autour des modèles de récupération de données et d'authentification.
Phase 5 : Interactivité client
Extrayez les îlots interactifs dans les Client Components. C'est la partie la plus difficile — vous devez identifier la limite client minimale.
// Avant : Tout était "client" par défaut dans Pages Router
// Après : Limites explicites
// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<article>
<h1>{product.name}</h1>
<ProductGallery images={product.images} /> {/* Client */}
<div dangerouslySetInnerHTML={{ __html: product.description }} /> {/* Server */}
<PricingWidget product={product} /> {/* Client */}
<Suspense fallback={<Skeleton />}>
<RelatedProducts categoryId={product.categoryId} /> {/* Server */}
</Suspense>
</article>
);
}
Si vous avez besoin d'aide pour planifier une stratégie de migration, notre équipe a fait cela assez souvent pour savoir où se trouvent les pièges — contactez-nous et nous pouvons discuter de votre architecture spécifique.
Implications SEO techniques
Avec 12+ années d'observation de la façon dont les moteurs de recherche gèrent le rendu JavaScript, je peux vous dire : le modèle RSC est la meilleure chose qui soit arrivée au SEO technique depuis SSR lui-même.
Voici pourquoi :
Les Server Components rendent du HTML complet sur le serveur. Googlebot obtient le contenu complet sans exécuter aucun JavaScript. Ce n'est pas nouveau — SSR a fait cela aussi. Mais les RSCs le font avec beaucoup moins de JavaScript côté client, ce qui affecte directement les Core Web Vitals.
Google a confirmé que l'INP (Interaction to Next Paint) est un signal de classement depuis mars 2024. Nos données de production montrent que les pages lourdes en RSC se notent 47% mieux en INP que les pages SSR équivalentes. Moins de JavaScript = moins de contention du thread principal = meilleur INP.
Le streaming affecte le comportement de crawl. Googlebot supporte le streaming HTTP depuis 2023, mais il a un timeout. Si votre limite Suspense la plus lente prend 15 secondes, Googlebot pourrait ne pas l'attendre. Gardez le contenu SEO critique en dehors des limites Suspense, ou assurez-vous que vos fallbacks Suspense contiennent du contenu significatif.
Pour les clients où le SEO est une préoccupation primaire, nous recommandons souvent notre approche de développement CMS headless couplée au App Router — le contenu vit dans un CMS, se rend via des Server Components, et n'expédie zéro JavaScript inutile au navigateur. C'est le meilleur des tous les mondes pour la performance de recherche.
Astro mérite d'être considéré si votre site est principalement axé sur le contenu avec une interactivité minimale. Mais pour les applications avec des fonctionnalités interactives riches, Next.js 16 avec les RSCs atteint le point idéal.
FAQ
Quelle est la différence entre SSR et RSC dans Next.js 16 ?
SSR (Server Side Rendering) est une stratégie de rendu qui détermine quand votre HTML de page est généré — à chaque requête, sur le serveur. React Server Components (RSC) sont un type de composant qui détermine quel code est expédié au navigateur. Dans le App Router, ils fonctionnent ensemble : les RSCs définissent ce qui a besoin de JavaScript client, et SSR gère la génération HTML. Vous utilisez généralement les deux simultanément.
Est-ce que les React Server Components remplacent le Server Side Rendering ?
Non. Les RSCs et SSR sont complémentaires, pas concurrents. Dans le App Router de Next.js 16, chaque page utilise SSR pour la réponse HTML initiale. Les RSCs déterminent quels composants dans cette page ont besoin d'envoyer du JavaScript au client pour l'hydratation. Vous pouvez avoir une page complètement SSR'd faite entièrement de Server Components (pas de JS client) ou un mélange des deux.
Combien les React Server Components réduisent-ils la taille du bundle ?
Dans nos mesures de production, les pages du App Router basées sur RSC avaient des bundles JavaScript en moyenne 63% plus petits comparés aux implémentations équivalentes du Pages Router. Les économies dépendent fortement de votre arborescence de composants — les pages avec beaucoup de contenu d'affichage seulement voient les plus grands gains, tandis que les pages hautement interactives (tableaux de bord, éditeurs) voient des améliorations plus petites.
Dois-je migrer mon application Next.js existante vers le App Router ?
Cela dépend de vos points sensibles. Si vos Core Web Vitals souffrent à cause de grands bundles JavaScript, ou si votre TTFB est élevé à cause de récupération de données séquentielle, la migration en vaut la peine. Si votre application Pages Router fonctionne bien et votre équipe est productive, il n'y a pas d'urgence. Next.js supporte les deux routers simultanément, donc vous pouvez migrer progressivement.
Comment fonctionne la mise en cache avec les Server Components dans Next.js 16 ?
Next.js 16 a simplifié le modèle de mise en cache de manière significative. Les Server Components peuvent être mis en cache statiquement (défaut pour les données statiques), révalidés sur une base de temps (ISR), ou rendus frais par requête (dynamique). Vous contrôlez cela au niveau du fetch avec next: { revalidate } ou au niveau du segment de route avec export const dynamic. Soyez prudent : une fonction dynamique dans un segment rend le segment entier dynamique.
Est-ce que les Server Components affectent le SEO ?
Les Server Components sont excellents pour le SEO. Ils rendent du HTML complet sur le serveur, que les moteurs de recherche peuvent indexer sans exécuter de JavaScript. De plus, le JavaScript côté client réduit améliore les scores des Core Web Vitals, particulièrement INP et TTI, qui sont des signaux de classement. La seule mise en garde est que le contenu dans les limites Suspense se diffuse progressivement, donc assurez-vous que le contenu SEO critique n'est pas derrière des récupérations de données lentes.
Puis-je utiliser les React Server Components avec un CMS headless ?
Absolument — c'est l'une des meilleures paires. Les Server Components peuvent récupérer directement le contenu du CMS au niveau du composant sans exposer les clés API ou le code SDK du CMS au client. Les bibliothèques comme le SDK Contentful, le client Sanity, ou le client @prismicio/client de Prismic restent entièrement sur le serveur. Combiné avec ISR ou une révalidation à la demande via webhooks, vous obtenez des pages rapides et mises en cache avec zéro JavaScript client inutile.
Quels sont les plus grands pièges lors de l'utilisation de RSC en production ?
Les trois problèmes majeurs que j'ai rencontrés : (1) Récupération de données cascade accidentelle dans les Server Components imbriqués — profiler et corriger avec React DevTools et les en-têtes de timing du serveur. (2) Rendre accidentellement les pages mises en cache dynamiques en utilisant cookies() ou headers() dans un composant imbriqué. (3) Erreurs de sérialisation de props lors du passage de données non sérialisables (fonctions, instances de classe, Dates) des Server Components vers les Client Components. Construisez de bonnes règles de linting et des conventions de limites de composants tôt.