Nehos Groupe

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

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··next-headless

#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 await directement 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, useContext ou tout hook React basé sur l'état local
  • useEffect, 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)
getServerSidePropsasync Server Component avec fetch sans cache
getStaticProps + revalidatefetch avec next: { revalidate: N }
useEffect + fetch client-sideServer Component avec fetch direct
SWR / React Query pour data freshServer Component + Suspense + streaming
SWR pour mutations optimistesServer Action + useOptimistic
Redux pour state globalZustand / 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étriqueAvantAprèsGain
Bundle JS client890 KB310 KB-65 %
LCP médian3.1 s1.6 s-48 %
TTFB médian580 ms95 ms-84 %
Score Lighthouse perf.6189+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étriqueAvantAprèsGain
Bundle JS client1.2 MB720 KB-40 %
LCP médian2.8 s2.1 s-25 %
INP230 ms110 ms-52 %
Requêtes DB/page4.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étriqueAvantAprèsGain
LCP (pages produit)2.4 s1.1 s-54 %
TTFB420 ms68 ms-84 %
Taux de rebond38 %29 %-24 %
Taux de conversion2.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.

Questions & Réponses

Questions fréquentes sur les React Server Components

Non. Les hooks qui reposent sur le cycle de vie client (useState, useEffect, useContext, useRef pour les interactions DOM…) ne sont pas disponibles dans les Server Components. Si vous avez besoin de ces hooks, le composant doit être marqué 'use client'. C'est précisément le critère pour décider où placer la frontière client/serveur.
Avec le SSR classique (Pages Router), le composant est rendu côté serveur *et* côté client : son code JavaScript est inclus dans le bundle envoyé au navigateur pour l'hydratation. Un Server Component ne s'exécute que côté serveur et n'est jamais inclus dans le bundle client. Ses dépendances (librairies de parsing, ORM, etc.) ne coûtent rien en téléchargement côté client.
React Query et SWR restent utiles dans les Client Components pour les données nécessitant du polling, des mutations optimistes ou une synchronisation temps réel. Mais pour les fetches de données statiques ou revalidées périodiquement, le fetch direct dans un Server Component est plus performant et plus simple à maintenir. Les deux approches coexistent dans une même application.
L'authentification se gère dans le Middleware Next.js (vérification du token JWT ou session, redirection si non authentifié) et dans les layouts Server Components via les helpers de session serveur. Les Server Components peuvent lire les cookies et headers directement via les fonctions `cookies()` et `headers()` de Next.js, sans exposer de logique d'authentification au client.
L'App Router de Next.js coexiste avec le Pages Router dans le même projet. La migration peut être progressive : des nouvelles routes créées en App Router (RSC), les anciennes gardées en Pages Router. À l'intérieur d'une route App Router, la migration est également incrémentale — un composant à la fois peut être converti en Server Component en retirant les hooks et en vérifiant qu'il n'a pas besoin d'APIs browser.
Les Server Components nécessitent un runtime Node.js persistant pour l'exécution serveur. Vercel, Railway, Fly.io, AWS (via Lambda ou ECS), Google Cloud Run et tout hébergeur supportant Node.js sont compatibles. L'export statique (`next export`) n'est pas compatible avec les Server Components dynamiques — il faut un hébergeur capable d'exécuter du code serveur.
Les Server Components s'exécutent côté serveur — leurs logs apparaissent dans le terminal de développement, pas dans la console navigateur. Next.js DevTools (disponible en Next.js 16) expose un panneau dédié avec la timeline des renders serveur, les données fetchées et les erreurs RSC. En production, les erreurs serveur sont loguées côté infrastructure (Vercel Logs, Datadog, etc.) et non exposées au client.
Réserver un audit