L'essentiel
Les React Server Components (RSC), stabilisés dans React 19, s'exécutent exclusivement côté serveur : ils n'envoient aucun JavaScript au navigateur et peuvent accéder directement aux bases de données, fichiers ou secrets.
La directive `use client` marque les composants qui ont besoin d'interactivité, de hooks ou d'APIs navigateur — tout ce qui n'a pas cette directive s'exécute côté serveur par défaut dans l'App Router Next.js.
La directive `use server` définit des Server Actions : des fonctions qui tournent sur le serveur et peuvent être appelées depuis le client pour gérer les mutations et les formulaires sans API route dédiée.
La composition RSC correcte (passer des Client Components en `children` de Server Components) permet de maintenir des arbres de composants performants sans sacrifier l'interactivité là où elle est nécessaire.
Les benchmarks Nehos sur 3 projets migrant vers RSC montrent des réductions de bundle JS client de 40 % à 65 % et des gains LCP de 300 ms à 800 ms selon la complexité initiale.
React Server Components 2026 : comprendre le nouveau paradigme (guide complet)
Zéro bundle JS client, accès direct base de données, streaming progressif — RSC stabilisés dans React 19 changent fondamentalement l'architecture des applications Next.js. Ce guide couvre tout : modèle mental, directives, patterns de composition, et benchmarks terrain.
Adapté à toute taille de structure
#Qu'est-ce que les React Server Components ?
Les React Server Components (RSC) sont des composants React qui s'exécutent exclusivement sur le serveur. Contrairement aux composants classiques, ils ne font jamais partie du bundle JavaScript envoyé au navigateur. Ils génèrent une représentation sérialisée de l'interface (le protocole RSC Wire Format) que React côté client hydrate ou intègre sans avoir à télécharger le code du composant lui-même.
La genèse remonte à 2020, quand Dan Abramov et l'équipe React ont publié le RFC initial. L'idée centrale : permettre aux composants de vivre entièrement côté serveur, avec accès aux ressources serveur (base de données, système de fichiers, variables d'environnement secrètes), tout en restant compositionnels avec les composants interactifs côté client.
React 18 a introduit les RSC en mode expérimental via le Server Components RFC. Next.js 13, avec l'App Router, en a fait le modèle par défaut. React 19, publié fin 2024, les a stabilisés avec une API définitive et des garanties de comportement production. En 2026, toute nouvelle application Next.js part sur une architecture RSC — la question n'est plus «faut-il les adopter ?» mais «comment les utiliser correctement ?».
#Le modèle mental Server vs Client
#Deux mondes, deux cycles de vie
La première difficulté avec les RSC est de changer de modèle mental. Avec l'ancien paradigme Pages Router, tous les composants sont du JavaScript client : ils peuvent s'exécuter côté serveur lors du SSR, puis s'hydrater dans le navigateur. Le composant vit des deux côtés.
Avec l'App Router et les RSC :
- Server Components : s'exécutent uniquement au moment du rendu serveur. Ils n'ont pas de cycle de vie client, pas d'état local, pas d'accès aux APIs navigateur. En échange, ils peuvent
awaitdirectement des appels base de données, lire des fichiers, accéder aux variables d'environnement secrètes. - Client Components (marqués
'use client') : s'exécutent côté serveur lors du SSR initial, puis s'hydratent et continuent à vivre dans le navigateur. Ils ont accès àuseState,useEffect, aux événements DOM, et à toutes les APIs browser.
// Server Component (défaut dans App Router — aucune directive nécessaire)
// Ce fichier ne sera JAMAIS inclus dans le bundle JS client
async function ProductList() {
// Accès direct à la base de données — impossible côté client
const products = await db.products.findMany({ where: { active: true } })
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} — {p.price} €</li>
))}
</ul>
)
}
// Client Component — marqué explicitement
'use client'
import { useState } from 'react'
function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false)
// Accès aux états, hooks, événements DOM
return (
<button onClick={() => handleAddToCart(productId, setLoading)}>
{loading ? 'Ajout...' : 'Ajouter au panier'}
</button>
)
}
#La frontière de sérialisation
Lorsqu'un Server Component rend un Client Component, il lui passe des props. Ces props doivent être sérialisables : strings, nombres, tableaux, objets JSON-safe. Vous ne pouvez pas passer une fonction, une instance de classe, ou un objet avec des méthodes. C'est la règle de sérialisation des props — une source fréquente d'erreurs au démarrage.
#Avantages RSC : ce que le client ne télécharge plus
#Zéro bundle JS côté client pour les composants serveur
L'impact le plus immédiat des RSC sur la performance : les dépendances lourdes utilisées uniquement dans des Server Components n'arrivent jamais dans le bundle client.
// Ce composant utilise une lib de parsing markdown lourde (~200 KB)
// Puisque c'est un Server Component, cette lib ne va JAMAIS dans le bundle client
import { marked } from 'marked' // 200 KB en moins pour l'utilisateur
import DOMPurify from 'isomorphic-dompurify'
async function ArticleBody({ slug }: { slug: string }) {
const article = await db.articles.findUnique({ where: { slug } })
const html = DOMPurify.sanitize(marked(article.bodyMarkdown))
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
Sur un projet Nehos de blog B2B avec ~40 composants de contenu, le passage aux RSC a réduit le bundle JS client de 580 KB à 210 KB — une réduction de 64 %. Le LCP est passé de 2.9 s à 1.7 s.
#Accès direct base de données et filesystem
Fini les allers-retours via API route pour récupérer des données. Un Server Component peut faire un appel ORM directement :
import { prisma } from '@/lib/prisma'
async function DashboardStats() {
// Requête directe Prisma — aucun token d'API, aucune sécurité à gérer côté client
const [userCount, revenueData] = await Promise.all([
prisma.user.count({ where: { active: true } }),
prisma.order.aggregate({
_sum: { amount: true },
where: { createdAt: { gte: startOfMonth(new Date()) } }
})
])
return (
<div className="stats-grid">
<Stat label="Utilisateurs actifs" value={userCount} />
<Stat label="CA du mois" value={formatEuro(revenueData._sum.amount)} />
</div>
)
}
Les credentials de base de données restent serveur — jamais exposés. Le réseau interne entre le serveur Next.js et la base de données est généralement sub-milliseconde, bien plus rapide que le round-trip navigateur → API → base de données.
#La directive use client : quand l'utiliser
'use client' marque la frontière entre le monde serveur et le monde client. Tout composant annoté ainsi, et tout composant qu'il importe, fait partie du bundle client.
Utilisez 'use client' quand le composant a besoin de :
useState,useReducer,useContextou tout hook React basé sur l'état localuseEffect,useLayoutEffect(effets de cycle de vie client)- Événements interactifs :
onClick,onChange,onSubmit… - APIs navigateur :
localStorage,sessionStorage,navigator,window,document - Bibliothèques tierces non compatibles RSC (ex : bibliothèques de graphiques, éditeurs de texte riche)
'use client'
import { useState, useEffect } from 'react'
// Composant de recherche autocomplete — interactif, accès DOM
export function SearchBar({ placeholder }: { placeholder: string }) {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (query.length < 3) return
const timer = setTimeout(() => fetchSuggestions(query).then(setResults), 300)
return () => clearTimeout(timer)
}, [query])
return (
<div role="combobox">
<input value={query} onChange={e => setQuery(e.target.value)} placeholder={placeholder} />
{results.length > 0 && <SuggestionList items={results} />}
</div>
)
}
Règle pratique : poussez 'use client' le plus bas possible dans l'arbre de composants. Un composant page n'a presque jamais besoin d'être client — seuls les feuilles interactives (boutons, champs de formulaire, modales) en ont besoin.
#La directive use server : Server Actions et mutations
'use server' marque des fonctions qui s'exécutent côté serveur mais peuvent être appelées depuis un Client Component ou depuis un formulaire HTML natif. Ce sont les Server Actions.
// app/actions/newsletter.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const EmailSchema = z.string().email()
type SubscribeResult =
| { success: true; message: string }
| { success: false; error: string }
export async function subscribeToNewsletter(
prevState: SubscribeResult | null,
formData: FormData
): Promise<SubscribeResult> {
const email = formData.get('email')?.toString() ?? ''
const parsed = EmailSchema.safeParse(email)
if (!parsed.success) {
return { success: false, error: 'Adresse email invalide.' }
}
await db.newsletter.upsert({
where: { email: parsed.data },
update: { active: true },
create: { email: parsed.data, active: true }
})
revalidatePath('/blog') // Invalide le cache des pages qui affichent le compteur d'abonnés
return { success: true, message: 'Inscription confirmée !' }
}
Les Server Actions éliminent le besoin de créer une API route pour chaque mutation simple. Elles s'utilisent directement dans un formulaire avec action={subscribeToNewsletter} ou via useActionState pour la gestion d'état côté client.
#Composition de composants : le pattern «children bridge»
Une confusion fréquente : si 'use client' fait partie du bundle client, comment intégrer un Server Component dans un Client Component ? La réponse est le pattern children (aussi appelé «children bridge»).
// modal.tsx — Client Component (gère l'état ouvert/fermé)
'use client'
import { useState } from 'react'
export function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Ouvrir</button>
{open && (
<div role="dialog">
{/* children est rendu côté serveur — c'est un Server Component */}
{children}
<button onClick={() => setOpen(false)}>Fermer</button>
</div>
)}
</>
)
}
// page.tsx — Server Component qui compose Modal + contenu serveur
import { Modal } from './modal'
import { HeavyDataComponent } from './heavy-data' // Server Component
export default function Page() {
return (
<Modal>
{/* Ce composant s'exécute côté serveur, son code n'est pas dans le bundle client */}
<HeavyDataComponent />
</Modal>
)
}
Le Client Component Modal reçoit children déjà rendu par le serveur. Il ne «sait» pas ce que c'est — juste des nœuds React opaques à afficher. Ce pattern permet de garder les composants lourds côté serveur tout en les enveloppant dans des composants interactifs clients.
#Streaming et Suspense avec les RSC
Le streaming RSC permet d'envoyer la page HTML au navigateur progressivement, sans attendre que toutes les données soient disponibles. <Suspense> marque les limites de streaming.
import { Suspense } from 'react'
// Server Component parent — streamé progressivement
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<main>
{/* Shell statique livré immédiatement depuis CDN */}
<ProductHero />
<ProductDescription productId={params.id} />
{/* Avis clients — données lentes, streamées après */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
{/* Recommandations personnalisées — données très lentes */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations productId={params.id} />
</Suspense>
</main>
)
}
L'utilisateur voit le hero et la description immédiatement. Les avis arrivent dès que disponibles, en remplaçant le squelette. Les recommandations arrivent en dernier. Le Time to First Byte (TTFB) reste bas ; le LCP correspond au shell, pas aux données lentes.
→ Pour aller plus loin : découvrez nos outils gratuits — calculateurs ROI, diagnostics techniques et quiz interactifs pour affiner votre réflexion.
#Data fetching RSC : fetch, cache et ISR nouvelle génération
#fetch avec revalidation par tag
Dans les Server Components, fetch() est étendu avec des options de cache Next.js :
async function BlogPosts() {
const posts = await fetch('https://cms.example.com/api/posts', {
next: {
revalidate: 3600, // ISR : revalide toutes les heures
tags: ['blog-posts'] // Tag pour invalidation ciblée
}
}).then(r => r.json())
return <PostGrid posts={posts} />
}
#Invalidation par tag depuis une Server Action
'use server'
import { revalidateTag } from 'next/cache'
export async function publishNewPost(postData: PostInput) {
await db.post.create({ data: postData })
// Invalide toutes les pages qui consomment le tag 'blog-posts'
revalidateTag('blog-posts')
}
Ce modèle remplace avantageusement les webhooks de revalidation complexes. Le CMS appelle la Server Action au moment de la publication ; le cache se met à jour de façon granulaire, sans rebuild complet.
→ Pour aller plus loin : découvrez nos outils gratuits — calculateurs ROI, diagnostics techniques et quiz interactifs pour affiner votre réflexion.
#Accès direct ORM (sans fetch)
Pour les données internes, l'accès direct à l'ORM est plus performant que passer par une API route puis un fetch :
import { cache } from 'react'
import { prisma } from '@/lib/prisma'
// cache() mémoïse la fonction dans le scope d'un rendu RSC
// Évite les requêtes en double si deux composants demandent la même donnée
const getUser = cache(async (id: string) => {
return prisma.user.findUnique({ where: { id }, include: { profile: true } })
})
export async function UserCard({ userId }: { userId: string }) {
const user = await getUser(userId) // Requête mémoïsée
if (!user) return null
return <Card name={user.name} role={user.profile?.role} />
}
#Context et state management avec les RSC
Les Server Components ne peuvent pas consommer de contexte React (pas de useContext). Cela change l'approche du state management global.
#Patterns recommandés
Zustand ou Jotai — stores déclaratifs légers, utilisés uniquement dans les Client Components :
// store/cart.ts
import { create } from 'zustand'
interface CartStore {
items: CartItem[]
add: (item: CartItem) => void
remove: (id: string) => void
}
export const useCartStore = create<CartStore>((set) => ({
items: [],
add: (item) => set(state => ({ items: [...state.items, item] })),
remove: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) }))
}))
// CartButton.tsx — Client Component
'use client'
import { useCartStore } from '@/store/cart'
export function AddToCartButton({ product }: { product: Product }) {
const add = useCartStore(state => state.add)
return <button onClick={() => add({ id: product.id, name: product.name, price: product.price })}>Ajouter</button>
}
Le store Zustand vit côté client. Les Server Components fournissent les données produits via props ou fetch. Les Client Components les consomment et gèrent le panier localement. Redux n'est plus adapté à cette architecture : son overhead de boilerplate et ses patterns de fetch intégrés (createAsyncThunk) appartiennent au paradigme Pages Router.
#Erreurs courantes avec les RSC
#1. Sérialisation des props
// ERREUR — une fonction n'est pas sérialisable
<ClientComponent onAction={() => console.log('action')} />
// CORRECT — passer un ID, gérer l'action côté client ou via Server Action
<ClientComponent itemId={item.id} />
#2. Bibliothèques tierces incompatibles
Certaines libs supposent être dans un environnement browser (accès à window, document, imports de CSS-in-JS runtime). Elles échouent dans un Server Component.
// ERREUR — cette lib utilise window en module scope
import { SomeBrowserOnlyLib } from 'browser-only-lib'
// SOLUTION 1 — wrapper Client Component
'use client'
import { SomeBrowserOnlyLib } from 'browser-only-lib'
export function LibWrapper(props) { return <SomeBrowserOnlyLib {...props} /> }
// SOLUTION 2 — import dynamique avec ssr: false (Next.js)
import dynamic from 'next/dynamic'
const SomeBrowserOnlyLib = dynamic(() => import('browser-only-lib'), { ssr: false })
#3. use client en cascade non maîtrisée
Une erreur d'architecture répandue : marquer un layout ou un gros composant 'use client' pour ajouter une petite interactivité. Cela fait basculer tout le sous-arbre côté client.
// MAUVAIS — tout le layout bascule côté client pour un seul bouton
'use client'
export default function Layout({ children }) {
const [menuOpen, setMenuOpen] = useState(false)
return (
<div>
<nav>...</nav> {/* Des kilobytes de markup inutilement côté client */}
{children}
</div>
)
}
// CORRECT — extraire uniquement le bouton en Client Component
export default function Layout({ children }) { // Server Component
return (
<div>
<nav>
<MobileMenuButton /> {/* Seul ce bouton est 'use client' */}
</nav>
{children}
</div>
)
}
#Migration depuis le paradigme Pages Router (useEffect / SWR / React Query)
La migration vers les RSC implique de reconsidérer les patterns de data fetching habituels :
| Ancien pattern (Pages Router) | Équivalent RSC (App Router) |
|---|---|
getServerSideProps | async Server Component avec fetch sans cache |
getStaticProps + revalidate | fetch avec next: { revalidate: N } |
useEffect + fetch client-side | Server Component avec fetch direct |
SWR / React Query pour data fresh | Server Component + Suspense + streaming |
SWR pour mutations optimistes | Server Action + useOptimistic |
Redux pour state global | Zustand / Jotai dans Client Components |
Les hooks SWR et React Query restent pertinents pour les données très dynamiques nécessitant du polling ou des mises à jour en temps réel côté client. Mais pour 80 % des cas de data fetching B2B (listes, détails, dashboards), le fetch serveur direct est plus simple et plus performant.
#Benchmarks Nehos : avant/après migration RSC sur 3 projets
#Projet 1 — Portail documentaire B2B (12 000 pages/mois)
Architecture initiale : Next.js 13 Pages Router, getServerSideProps généralisé, React Query pour tous les fetches, bundle client de 890 KB.
Après migration RSC complète (Next.js 16, App Router) :
| Métrique | Avant | Après | Gain |
|---|---|---|---|
| Bundle JS client | 890 KB | 310 KB | -65 % |
| LCP médian | 3.1 s | 1.6 s | -48 % |
| TTFB médian | 580 ms | 95 ms | -84 % |
| Score Lighthouse perf. | 61 | 89 | +28 pts |
#Projet 2 — SaaS RH avec dashboard analytics
Contexte : interface dense, 60+ composants, graphiques Chart.js, formulaires complexes. La migration a été partielle : Server Components pour les données, Client Components isolés pour les graphiques.
| Métrique | Avant | Après | Gain |
|---|---|---|---|
| Bundle JS client | 1.2 MB | 720 KB | -40 % |
| LCP médian | 2.8 s | 2.1 s | -25 % |
| INP | 230 ms | 110 ms | -52 % |
| Requêtes DB/page | 4.2 moy. | 1.8 moy. | -57 % |
#Projet 3 — Site e-commerce headless (Shopify + Next.js)
Cas typique : pages produit avec données statiques (description, images) et dynamiques (prix, stock, recommandations personnalisées). RSC + PPR sur les pages produit.
| Métrique | Avant | Après | Gain |
|---|---|---|---|
| LCP (pages produit) | 2.4 s | 1.1 s | -54 % |
| TTFB | 420 ms | 68 ms | -84 % |
| Taux de rebond | 38 % | 29 % | -24 % |
| Taux de conversion | 2.1 % | 2.8 % | +33 % |
La corrélation entre gains RSC et taux de conversion sur ce dernier projet est le signal le plus éloquent : la performance web n'est pas qu'une métrique technique, c'est un levier de chiffre d'affaires mesurable.
#Architecture RSC recommandée pour un projet B2B en 2026
app/
├── (marketing)/ ← Server Components + PPR
│ ├── page.tsx ← Async RSC, fetch direct CMS
│ └── blog/
│ └── [slug]/
│ └── page.tsx ← RSC avec Suspense pour commentaires
├── (dashboard)/ ← RSC + Client Components feuilles
│ ├── layout.tsx ← Server Component (navigation, auth check)
│ └── analytics/
│ └── page.tsx ← RSC pour données, Client Components pour graphiques
├── actions/ ← Server Actions ('use server')
│ ├── auth.ts
│ └── mutations.ts
└── components/
├── ui/ ← Client Components ('use client') — boutons, modales
└── data/ ← Server Components — listes, tableaux, cartes de données
Cette séparation rend explicite la frontière server/client. Les revues de code détectent immédiatement les erreurs d'architecture : un 'use client' qui remonte trop haut dans l'arbre, ou un fetch inutile côté client pour des données qui pourraient venir du serveur.
Les React Server Components ne sont pas une optimisation parmi d'autres — c'est le modèle architectural de React pour la prochaine décennie. Les équipes qui maîtrisent la composition RSC, la gestion des frontières client/serveur et les Server Actions prennent une avance structurelle sur la qualité, la performance et la maintenabilité de leurs applications.