Components serveurs React : l’architecture derrière le rendu sans bundle
Comprendre les raisons qui sous-tendent les composants serveur React, des problèmes de taille du bundle et de flux en cascade aux limites entre le serveur et le client ainsi qu’aux chargements des RSC.
Si vous avez passé du temps à essayer de comprendre les React Server Components et que vous êtes encore plus perdu qu’au début, ce n’est pas signe que vous manquez quelque chose d’évident. C’est plutôt le signe que les explications que vous avez trouvées faisaient partie du problème. Pendant quelques années, la présentation officielle a décrit les Server Components comme des « composants qui s’affichent sur le serveur », une expression qui ressemble étrangement à ce que getServerSideProps ou le rendu serveur classique faisaient déjà dans Next.js. Ensuite, on a affirmé que ces composants « n’envoient aucun JavaScript au client », ce qui sonne comme un tour de passe-passe lié aux performances plutôt que comme une nouvelle façon de structurer une application. Puis est arrivé l’App Router, et soudainement chaque fichier dans un projet Next.js est devenu par défaut un Server Component, à moins d’ajouter une chaîne de caractères spéciale en haut du fichier.
Cette confusion n’était pas une faute de votre part. Elle est survenue parce qu’un paradigme véritablement nouveau était décrit à l’aide d’un vocabulaire emprunté à un paradigme plus ancien. Ce guide vise à combler cette lacune. Lorsque vous aurez terminé sa lecture, vous devriez comprendre non seulement le fonctionnement des composants serveur, mais aussi les raisons qui font d’eux une manière complètement différente de penser aux applications React.
Le problème que RSC résout réellement
Le piège de la taille du bundle
In the classic React model, every component you author turns into JavaScript that gets shipped to the browser, no exceptions. It makes no difference whether that component just renders some static markdown, does a simple date calculation, or hits a database. As long as it lives somewhere in your component tree, its code lives in your bundle too.
That creates an unforgiving trade-off. Adding more components inflates your JavaScript payload. A heavier payload pushes out your Time to Interactive. In practice, you end up delivering code to browsers that never had any reason to execute it.
The Waterfall Problem
Au préalable aux Server Components, tout composant ayant besoin de données devait les récupérer une fois arrivé du côté client. Le schéma typique consistait à afficher un conteneur de remplacement, à lancer un useEffect, à attendre une réponse, et seulement ensuite d’afficher le contenu réel. Si ce contenu incluait un composant imbriqué ayant également besoin de données, le même cycle se répétait : un autre effet, une autre attente, un nouveau délai ajouté au précédent. Cette réaction en chaîne constitue la cascade de récupération des données, et elle explique pourquoi de nombreuses applications React semblent lentes même lorsque leurs API backend répondent rapidement.
Une solution temporaire consistait à déplacer la récupération des données au niveau de la route en utilisant des outils tels que getServerSideProps ou des chargeurs de route, mais cela avait un inconvénient : cela altérait la nature autonome des composants. Soudain, la page devait connaître des données qui appartenaient conceptuellement à ses enfants, et les composants cessaient d’être des unités indépendantes.
Le coût de l’hydratation
Le rendu côté serveur traditionnel fonctionne en générant de l’HTML sur le serveur, en le transmettant, puis en rérendant toute l’application en JavaScript côté client pour la rendre interactive. Cela signifie que chaque composant, y compris ceux qui n’auront jamais besoin de comportements côté client, doit encore passer par l’hydratation. En pratique, on paie un coût d’interactivité même pour du contenu complètement statique.
Les composants serveur React résolvent ces trois problèmes au niveau de l’architecture, et non en tant que correctif progressif pour les performances.
Le modèle mental : les composants serveur ne sont pas « SSR 2.0 »
C’est là l’idée centrale sur laquelle repose tout ce guide : les composants serveur ne constituent pas un moyen de rendre des pages. Ce sont plutôt une catégorie distincte de composants.
React classique ne reconnaissait qu’un seul type de composant. Il s’exécutait dans le navigateur, pouvait gérer des états, exécuter des effets et répondre aux événements. Tout son cycle de vie, de la création à la destruction, avait lieu du côté client.
Les composants serveur React ajoutent une deuxième catégorie. Un composant serveur s’exécute sur le serveur. Il peut librement accéder au système de fichiers, interroger directement une base de données ou utiliser une bibliothèque fonctionnant uniquement à l’intérieur de Node.js. Ce qu’il ne peut pas faire, c’est utiliser useState, useEffect ou attacher des gestionnaires d’événements, car il ne s’exécute jamais dans le navigateur. Il n’y a pas de phase d’hydratation pour lui. Son code n’est même pas transmis au client sous forme de JavaScript.
Une façon utile d’imaginer cela : l’arbre des composants React est désormais composite. Certaines branches sont générées sur le serveur, tandis que d’autres le sont dans le navigateur. Les branches côté serveur s’affichent une seule fois, lors de la demande, et ce qu’elles produisent est transmis au client sous forme d’éléments React serialisés. Les branches côté client s’affichent comme d’habitude dans le navigateur et conservent toutes les fonctionnalités interactives auxquelles vous êtes habitué.
C’est un mécanisme différent de SSR. Le rendu du côté serveur génère l’ensemble de l’application en HTML sur le serveur, puis reconstitue tout cela dans le navigateur par la suite. En revanche, RSC ne rend que des composants spécifiques sur le serveur et n’envoie jamais leur code de base au navigateur.
Qu’est-ce qui est vraiment envoyé au client ?
Lorsqu’un composant serveur est rendu, il ne génère pas d’HTML. Au lieu de cela, il produit une description sérialisée de son output, appelée charge utile RSC — un flux d’instructions ressemblant à du JSON qui indique à React, du côté client, comment assembler l’arborescence qu’il doit afficher.
Voici la séquence d’événements qui se produit en arrière-plan lorsque l’on demande une page contenant des composants serveurs :
- React rend les composants serveurs sur le serveur.
Le point essentiel à retenir est que le code des composants serveur ne fait jamais partie du bundle JavaScript envoyé aux navigateurs. Imaginez un composant MarkdownRenderer basé sur une bibliothèque de parsing Markdown de 200 KB. Si ce composant est un composant serveur, toute la bibliothèque de 200 KB reste sur le serveur. Le navigateur ne voit que la structure générée par le parseur, jamais le parseur lui-même.
C’est là l’essence de l’affirmation concernant une taille de bundle nulle, et il ne s’agit pas d’une simple technique d’optimisation. Cela représente un véritable changement dans ce que signifie réellement la création d’une application React.
La frontière serveur-client
Dans App Router, chaque fichier est considéré comme un composant serveur à moins qu’on ne précise le contraire. Il ne s’agit pas simplement d’une option par défaut stylistique — c’est une hypothèse structurelle intégrée au framework, reflétant l’idée que la majeure partie de votre interface n’a pas besoin d’exécuter du tout dans le navigateur.
Un composant client, en revanche, est du code conçu pour s’exécuter sur la machine de l’utilisateur. Pour désigner un fichier de cette manière, il suffit d’ajouter la directive "use client" en haut du fichier. Il ne s’agit pas seulement de texte indicatif — elle constitue une limite stricte. Elle indique au compilateur de React que tout ce qui est défini dans ce fichier, ainsi que tout élément importé, doit être emballé et transmis au navigateur.
La règle qui assure la cohérence de tout ce système est la suivante : les composants serveur peuvent librement importer et afficher des composants client, mais l’inverse n’est jamais autorisé — un composant client ne peut pas importer un composant serveur.
À première vue, cela semble contraire à l’intuition. Le code côté serveur ne devrait-il pas être utilisable de n’importe où, y compris dans les fichiers clients ? En réalité, ce n’est pas le cas si l’on suit le parcours réel des données :
- D’abord, un composant serveur s’exécute, avec un accès direct à votre base de données et aux ressources backend. Il récupère les données nécessaires et crée un layout. Au sein de ce layout, il peut afficher quelque chose comme
<UserProfile />, ce qui nécessite de l’interactivité — c’est pourquoi cette partie est écrite en tant que composant client. Le composant serveur parent transmet ensuite les données utilisateur récupérées sous forme de props.
C’est pourquoi une telle organisation ne peut pas fonctionner :
// ❌ Impossible: Client Component importing Server Component
'use client';
import { ServerDataFetcher } from './ServerDataFetcher'; // This breaks!
export function ClientWidget() {
return (
<div>
<ServerDataFetcher /> {/* Cannot render a server component here */}
</div>
);
}
Mais structurer les choses de l’autre manière est tout à fait valide :
// ✅ Correct: Server Component importing Client Component
// This is a Server Component (no 'use client')
import { ClientWidget } from './ClientWidget';
async function ServerDataFetcher() {
const data = await db.query('SELECT * FROM posts');
return (
<div>
<h1>Latest Posts</h1>
{data.map(post => (
<ClientWidget key={post.id} post={post} />
))}
</div>
);
}
Le schéma est simple : le composant serveur s’occupe de la récupération des données et de l’affichage structurel, puis il transmet les données aux composants interactifs situés à la base de l’arbre. Ces composants de base gèrent les clics, les soumissions de formulaires et les animations. L’architecture résultante est essentiellement un arbre affiché sur le serveur, avec des zones interactives disséminées à ses extrémités.
Composants asynchrones : la révolution de la récupération des données
React classique ne permettait jamais aux fonctions de composant d’être asynchrones. Écrire await directement à l’intérieur du corps d’un composant n’était pas une option — il fallait impérativement utiliser useEffect et gérer manuellement l’état de chargement.
Les composants serveur brisent cette limitation : ils peuvent être async. Il s’agit d’une petite modification syntaxique, mais elle transforme considérablement l’architecture sous-jacente.
// ✅ Server Component: Direct data access, no useEffect
async function BlogPostList() {
// This runs on the server. No fetch call in the browser.
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
);
}
Remarquez tout ce qui est absent ici. Il n’y a ni useState pour suivre un indicateur de chargement, ni useEffect pour déclencher une requête, ni d’écran squelette construit manuellement. Le composant convertit simplement les enregistrements de la base de données directement en éléments React. La récupération des données a désormais lieu au niveau de chaque composant plutôt que pour chaque route, et elle s’effectue entièrement du côté serveur, jamais dans le navigateur.
Cela fait disparaître le problème des chutes d’eau : le serveur peut traiter chaque composant asynchrone en même temps avant d’envoyer une réponse au client. Les appels à la base de données s’exécutent dans le même centre de données que votre backend, ce qui fait que la latence est mesurée en microsecondes plutôt qu’en millisecondes, comme c’est le cas pour les communications via une connexion mobile.
La directive “use client” : quand et pourquoi
L’ajout de "use client" indique qu’un fichier appartient à l’environnement de exécution du navigateur. Vous en avez besoin chaque fois qu’un composant interagit avec des éléments propres au côté client :
- L’état local via
useStateouuseReducer - Les hooks de cycle de vie tels que
useEffectouuseLayoutEffect - Gestion des événements, comme
onClickouonSubmit
localStorage, window, documentuseRouter ou des fonctions comme useMediaQueryL’erreur fréquente consiste à placer cette directive trop haut dans l’arborescence du composant. Les développeurs transforment souvent toute une page en composant client simplement parce qu’un bouton intégré nécessite de l’interactivité.
// ❌ Bad: Making the whole page client-side for one interactive element
'use client';
import { useState } from 'react';
import { HeroSection } from './HeroSection'; // Static, could be server
import { LikeButton } from './LikeButton'; // Interactive, needs client
export default function Page() {
return (
<div>
<HeroSection />
<LikeButton />
</div>
);
}
Dans cet extrait, HeroSection n’est qu’un marquage purement statique qui pourrait facilement rester généré côté serveur. Mais comme "use client" a été déclaré au niveau du parent, toutes les importations qui suivent — y compris cette section statique — sont tout de même regroupées et envoyées au navigateur.
La solution : placer "use client" plus bas dans l’arborescence
La solution consiste à conserver la page elle-même en tant que composant serveur et à traiter l’élément interactif comme un nœud feuille qui y est importé.
// ✅ Good: Page is server, only LikeButton is client
// page.tsx (Server Component by default)
import { HeroSection } from './HeroSection';
import { LikeButton } from './LikeButton';
export default function Page() {
return (
<div>
<HeroSection />
<LikeButton postId="123" />
</div>
);
}
// LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
Avec cette structure, ni HeroSection ni Page n’est jamais envoyé au navigateur sous forme de JavaScript. Seul LikeButton, accompagné de son appel à useState, est transmis. C’est le mécanisme pratique derrière l’objectif d’une taille de bundle nulle : laisser autant que possible l’arbre des composants sur le serveur, et réserver la partie client aux nœuds qui ont réellement besoin d’interactivité.
Intercalation : le modèle qui rend les RSC puissants
La technique qui confère à RSC sa véritable force est l’intercalation : imbriquer des composants serveur à l’intérieur de composants client, transmettre des données provenant du serveur sous forme de propriétés, et permettre à ces composants client d’exposer des slots children pouvant contenir davantage de composants serveur.
// Layout.tsx (Server Component)
import { Sidebar } from './Sidebar';
import { AnalyticsProvider } from './AnalyticsProvider';
export default async function DashboardLayout({ children }) {
// Fetch user data on the server
const user = await getCurrentUser();
const permissions = await getUserPermissions(user.id);
return (
<div className="dashboard">
<Sidebar user={user} permissions={permissions} />
{/* AnalyticsProvider is a Client Component */}
<AnalyticsProvider userId={user.id}>
{/* children here can be a Server Component page */}
<main>{children}</main>
</AnalyticsProvider>
</div>
);
}
// AnalyticsProvider.tsx
'use client';
import { createContext, useContext } from 'react';
const AnalyticsContext = createContext(null);
export function AnalyticsProvider({ userId, children }) {
// Client-side analytics initialization
useEffect(() => {
analytics.identify(userId);
}, [userId]);
return (
<AnalyticsContext.Provider value={{ userId }}>
{children}
</AnalyticsContext.Provider>
);
}
Ici, DashboardLayout s’exécute sur le serveur et interroge directement la base de données. Il transmet ces données à Sidebar, qui peut lui-même être un composant serveur ou client en fonction de ses besoins. Il enveloppe également le reste de la page dans AnalyticsProvider, un composant client nécessaire dans le navigateur pour initialiser une bibliothèque d’analytique tierce.
Le détail important est la propriété children. Tout ce qui est affiché à l’intérieur de AnalyticsProvider n’a pas besoin de devenir du code client simplement parce qu’il y est imbriqué. React transmet le résultat déjà affiché du composant serveur via le composant client sous forme de children — le composant client agit simplement comme un enveloppe ou une limite, et non comme un convertisseur qui force tout ce qu’il contient à s’exécuter du côté client.
C’est précisément ainsi que sont construites les interfaces utilisateur hybrides : du contenu généré sur le serveur est imbriqué dans des enveloppes réservées au client, là où une véritable interactivité est requise.
La charge utile RSC : un aperçu en détail
Le rendu des composants serveurs ne produit pas directement du HTML. Au lieu de cela, React émet la charge utile RSC, un flux binaire qui ressemble conceptuellement à ceci :
1:I["node_modules/react/jsx-runtime.js", "jsx"]
2:I["./components/ClientWidget.js", "default"]
0:["quot;, "div", null, {"children": [
["quot;, "h1", null, {"children": "Latest Posts"}],
["quot;, "@2", null, {"postId": "123", "title": "Hello World"}]
]}]
Chaque ligne de ce flux représente une instruction. Le symbole $ indique un élément React. I signifie une importation d’un module de composant client. Une référence comme @2 fait référence à la deuxième importation — dans ce cas, ClientWidget. Notez que le serveur a déjà entièrement rendu la balise h1, mais pour ClientWidget, il a simplement transmis les propriétés et indiqué l’endroit où se trouve son code, sans le rendre lui-même.
Dès que le navigateur reçoit ce flux, il résout ces références d’importation, récupère les bundles de composants clients nécessaires et assemble l’arbre de composants final. Comme les données sont transmises progressivement, le navigateur n’a pas besoin d’attendre la totalité avant de commencer à afficher le contenu. C’est précisément cette livraison progressive qui permet à RSC de prendre en charge l’affichage serveur par flux tout en contournant le goulot d’étranglement habituel lié à l’hydratation.
Cachage et révalidation
Next.js 15 et versions ultérieures donnent aux composants serveurs accès à l’ensemble des stratégies de cachage :
- Rendu statique (par défaut). À moins qu’un composant ne lise des données dynamiques ou n’effectue une requête non mémorisée, il est rendu statiquement au moment de la compilation.
// Cached indefinitely at build time
async function ProductList() {
const products = await fetch('https://api.example.com/products');
// ...
}
- Rendu dynamique. L’appel de
cookies(),headers()ou la lecture desearchParams, ainsi que la définition explicite deexport const dynamic = 'force-dynamic', obligent le composant à se rendre à chaque demande reçue. - Révalidation via un délai prédéfini.
// Revalidate every 60 seconds
async function ProductList() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
// ...
}
- Révalidation déclenchée sur demande.
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST() {
revalidatePath('/products');
return Response.json({ revalidated: true });
}
Le point essentiel est que le comportement du cache s’applique désormais à des composants individuels plutôt qu’à des routes entières. Deux composants situés sur la même page peuvent suivre des règles de mise en cache complètement différentes. Un tel niveau de précision n’existe pas dans le SSR classique, où un seul en-tête de cache gère l’ensemble de la page en même temps.
Malentendus persistants
« Les composants serveur existent principalement pour le SEO. » Pas tout à fait — une meilleure indexation des recherches est un effet secondaire, pas l’objectif. La véritable motivation est de réduire le JavaScript côté client et de permettre la récupération des données au niveau du composant.
« Tout composant qui utilise un hook a besoin de use client. » Cela n’est vrai que lorsque le hook dépend réellement des API du navigateur. Le hook use de React 19 peut déballer des promesses et lire le contexte directement à l’intérieur des composants serveur, de sorte que de nombreux hooks fonctionnent parfaitement sans jamais interagir avec le client.
« Les composants serveur rendent les routes API obsolètes. » Ce n’est pas le cas — les deux fonctionnent ensemble. Les composants serveur s’occupent de la récupération des données pour l’affichage initial, mais vous avez toujours besoin de routes API pour gérer les mutations, les webhooks en provenance de tiers, ainsi que toute récupération de données qui a lieu côté client après l’hydratation.
« Les composants serveur n’ont pas d’état. » Ils ne disposent pas d’un état de type navigateur — il n’y a pas de useState disponible — mais ils peuvent accéder librement à des sources d’état côté serveur telles que les bases de données, les caches, le système de fichiers et les variables d’environnement.
« Le contexte est interdit dans les composants serveur. » Il est vraiment impossible d’utiliser React Context à l’intérieur d’un composant serveur, car le contexte existe spécifiquement pour éviter le transfert de propriétés en profondeur dans les arbres rendus côté client. Ce que vous pouvez faire à la place, c’est transmettre des données sous forme de propriétés ordinaires, et comme le rendu serveur résout l’arbre des composants de manière synchrone du haut vers le bas, Next.js propose également des options comme unstable_rootParams en plus du transfert de propriétés classique.
La situation en 2026
Désormais que React 19 a atteint la stabilité, les outils associés ont considérablement mûri :
- Les actions serveur permettent à un composant client d’appeler une fonction asynchrone qui s’exécute directement sur le serveur, atténuant ainsi la frontière entre le client et le serveur en ce qui concerne les mutations.
- L’hook
useoffre aux composants clients un moyen de déballer les promesses et le contexte sans avoir recours àuseEffect. - Le pré-rendering partiel (PPR) dans Next.js 15 permet de servir une coquille statique directement depuis le CDN, tandis que les sections dynamiques sont chargées depuis le serveur d’origine.
- Le compilateur React gère automatiquement la mémorisation des composants clients, réduisant les appels manuels à
useMemoet rendant la transition entre le côté client et le côté serveur plus fluide.
Le modèle conceptuel s’est stabilisé à ce stade. Les composants serveur ne sont plus une fonctionnalité optionnelle — ils constituent l’hypothèse de base sur laquelle sont construites les applications React modernes. Les composants client sont désormais l’exception : une issue de secours délibérée réservée à la couche interactive.
Un changement d’architecture, pas seulement en termes de performance
Les composants serveur React ne sont pas simplement un ajustement de performance. Ils représentent une réflexion sur le lieu où le code React s’exécute réellement. Pendant environ dix ans, React a été fondamentalement une bibliothèque pour navigateur qui a été adaptée pour fonctionner du côté serveur principalement afin de répondre aux exigences SEO. Aujourd’hui, c’est quelque chose de différent : un système de composants full-stack qui fonctionne des deux côtés de la connexion réseau, chacun ayant des limites, des responsabilités et des profils de performance bien définis.
Le serveur a cessé d’être simplement un endroit d’où récupérer des données — il est désormais un véritable environnement de rendu. Et le navigateur n’est plus l’unique habitat des composants React ; c’est plutôt là que réside spécifiquement l’interactivité.
Lorsque cette approche prend forme, une grande partie de la confusion entourant RSC disparaît. La question passe de « Ai-je besoin de use client ici ? » à « À quel endroit cette logique particulière doit-elle être placée ? » C’est la question à se poser, et c’est cet état d’esprit qui permet aux applications de se développer.
Lectures complémentaires
- 20 modèles avancés de Next.js 16 pour l’architecture d’applications de niveau senior — Un aperçu du design basé sur le serveur, du cache, de la diffusion en flux continu, du PPR, des routes parallèles et d’interception, ainsi que d’autres modèles pour développer des applications Next.js 16 scalables.
- Le frontend en 2027 : le rendu basé sur le serveur, TypeScript et les paramètres par défaut d’Edge — Une analyse approfondie de la manière dont les frameworks basés sur le serveur, TypeScript obligatoire, le codage assisté par l’IA et le rendu à Edge transforment les pratiques de développement frontend.