Développement

Server Components vs Client Components Next.js : quand utiliser quoi en 2026

Publié le 7 May 2026 — 9 min de lecture
En bref

Server Components ou Client Components Next.js ? Guide pratique 2026 pour arbitrer correctement : isoler 'use client' en feuille, composer avec Suspense et Server Actions.

Depuis Next.js 13, l’App Router introduit deux mondes parallèles : les Server Components (rendus côté serveur, par défaut) et les Client Components (rendus dans le navigateur, opt-in via la directive 'use client'). Cette dichotomie est la décision architecturale la plus importante d’un projet Next.js moderne. Mal arbitrée, elle multiplie le bundle JS par 3 ou tue les Core Web Vitals. Bien arbitrée, elle permet d’atteindre un score Lighthouse 95+ avec des fonctionnalités riches. Voici comment décider.

Server Components vs Client Components : le rappel des bases

Ce qu’est un Server Component

Un Server Component est rendu intégralement côté serveur (au build pour SSG, à la requête pour SSR/ISR). Le navigateur reçoit du HTML déjà calculé, sans le JavaScript du composant lui-même. Conséquences directes :

  • Bundle JavaScript client réduit (parfois divisé par 3 à 5).
  • Accès direct à la base de données, au système de fichiers, aux secrets serveur.
  • Aucune interactivité : pas de useState, pas de useEffect, pas de gestionnaires d’événements.
  • Async natif : un Server Component peut être async et faire await fetch() ou await db.query().

Ce qu’est un Client Component

Un Client Component est rendu côté serveur lors du premier chargement (pour le HTML initial), puis hydraté et ré-exécuté côté navigateur. Il est marqué par la directive 'use client' en première ligne du fichier. Caractéristiques :

  • Le code source du composant est expédié dans le bundle JS du navigateur.
  • Tous les hooks React fonctionnent : useState, useEffect, useContext, useReducer.
  • Les gestionnaires d’événements (onClick, onSubmit) sont autorisés.
  • Pas d’accès direct au serveur — il faut passer par fetch(), des Server Actions, ou des routes API.

La règle par défaut en 2026

Dans l’App Router, tout est Server Component par défaut. Vous n’opt-in dans le client que si vous en avez un besoin précis et démontrable. C’est l’inverse mental du Pages Router (où tout était client). Cette inversion est volontaire : elle pousse à minimiser la surface JavaScript expédiée au navigateur.

Quand utiliser un Server Component ?

Par défaut, partout. Plus précisément, dès qu’un composant rentre dans l’un de ces cas :

Affichage de données stockées côté serveur

Liste de produits, fiche article, commentaires, données utilisateur. Au lieu de faire un useEffect + fetch + état de chargement, vous écrivez directement :

// app/products/page.tsx
import { db } from '@/lib/db';

export default async function ProductsPage() {
  const products = await db.product.findMany();
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Aucun JavaScript n’est expédié au client pour cette page. Pas de spinner, pas de waterfall, pas de re-render inutile.

Accès à des secrets ou variables d’environnement

Vous avez besoin d’une clé API privée (Stripe, OpenAI, Sendgrid) ? Server Component obligatoire. Tout ce qui est dans process.env.X_SECRET est inaccessible côté client par construction.

Calculs lourds qui ne dépendent pas de l’utilisateur

Parsing de Markdown, génération de PDF, traitement d’images, requêtes SQL agrégées. Faire tourner ça côté serveur évite d’expédier 50-200 Ko de bibliothèques inutiles au navigateur.

SEO critique

Le contenu visible doit apparaître dans le HTML initial pour être correctement indexé. Les Server Components garantissent que le bot Google reçoit le contenu sans avoir à exécuter le JavaScript.

Quand utiliser un Client Component ?

Quatre cas légitimes, et seulement quatre :

1. État local interactif

Toggle, modal, accordion, onglets, formulaires contrôlés. Tout ce qui demande useState ou useReducer.

'use client';
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

2. APIs navigateur

window, document, localStorage, navigator.geolocation, IntersectionObserver, WebSocket. Toute API qui n’existe que dans le navigateur.

3. Effets et abonnements

useEffect, abonnements à des stores (Zustand, Jotai), animations requestAnimationFrame, polling.

4. Bibliothèques tierces qui imposent le client

Charts (Recharts, Chart.js), maps (Mapbox, Leaflet), éditeurs riches (TipTap, Lexical), vidéo (Plyr, Video.js). La doc indique généralement 'use client' requis.

Le piège n°1 : “use client” qui contamine tout

Quand vous mettez 'use client' en haut d’un fichier, tout l’arbre descendant devient client par construction. Si votre layout.tsx de section est marqué 'use client', l’intégralité de la page passe en client, même les composants qui ne le devraient pas.

Anti-pattern fréquent

// ❌ MAUVAIS : layout entier en client à cause d'un Header animé
'use client';

import { motion } from 'framer-motion';
import { Article } from './Article'; // devient client malgré lui
import { Sidebar } from './Sidebar'; // devient client malgré lui

export default function Layout({ children }) {
  return (
    <motion.div animate={{ opacity: 1 }}>
      <Header />
      <Sidebar />
      {children}
    </motion.div>
  );
}

Bonne pratique : isoler le client en feuille

// ✅ BON : layout reste serveur, seul AnimatedShell est client
import { AnimatedShell } from './AnimatedShell';
import { Sidebar } from './Sidebar';

export default function Layout({ children }) {
  return (
    <AnimatedShell>
      <Sidebar />
      {children}
    </AnimatedShell>
  );
}

// AnimatedShell.tsx
'use client';
import { motion } from 'framer-motion';
export function AnimatedShell({ children }) {
  return <motion.div animate={{ opacity: 1 }}>{children}</motion.div>;
}

La règle : poussez 'use client' aussi bas que possible dans l’arbre. Idéalement, vos Client Components sont des feuilles, pas des conteneurs.

Composer Server et Client : le pattern children

Un Server Component peut rendre un Client Component, mais l’inverse n’est pas direct. Pour passer un sous-arbre serveur à un Client Component, utilisez la prop children :

// ServerData.tsx (Server Component)
import { db } from '@/lib/db';
export async function ServerData() {
  const data = await db.find();
  return <p>{data.value}</p>;
}

// Modal.tsx (Client Component)
'use client';
import { useState } from 'react';
export function Modal({ children }) {
  const [open, setOpen] = useState(false);
  return open ? <div>{children}</div> : null;
}

// page.tsx
import { Modal } from './Modal';
import { ServerData } from './ServerData';
export default function Page() {
  return (
    <Modal>
      <ServerData />
    </Modal>
  );
}

La Modal reste un Client Component avec son useState, mais son contenu (ServerData) reste rendu côté serveur. Le serveur compose le HTML et l’envoie au client qui n’hydratera que la coquille interactive.

Les Server Actions : le pont serveur depuis le client

Depuis Next.js 14, les Server Actions permettent à un Client Component d’invoquer une fonction serveur sans créer une API route. C’est l’équivalent moderne des form submissions :

// actions.ts
'use server';
export async function createPost(formData: FormData) {
  const title = formData.get('title');
  await db.post.create({ data: { title } });
}

// NewPostForm.tsx
'use client';
import { createPost } from './actions';
export function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button>Publier</button>
    </form>
  );
}

Avantages : pas de route API à maintenir, validation côté serveur native, support de la progressive enhancement (le formulaire fonctionne même JavaScript désactivé).

Le streaming et Suspense : meilleur des deux mondes

Avec <Suspense>, vous pouvez streamer le HTML d’un Server Component progressivement, en affichant un fallback pendant que les données arrivent :

import { Suspense } from 'react';

export default function Page() {
  return (
    <>
      <Header /> {/* HTML envoyé immédiatement */}
      <Suspense fallback={<ProductsSkeleton />}>
        <Products /> {/* await db.find(), streamé quand prêt */}
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>
    </>
  );
}

Le navigateur reçoit le header en premier, puis les produits dès qu’ils sont disponibles, puis les avis. C’est le LCP qui chute spectaculairement quand les requêtes sont indépendantes : 4s → 1,2s sur des pages typiques.

Comparatif : impact bundle et performance

Approche JS bundle LCP médian INP
App Router 100 % Server Components 40 – 80 Ko 1,1 s 50 ms
App Router avec ‘use client’ isolé 80 – 150 Ko 1,4 s 80 ms
Pages Router (legacy, tout client) 250 – 600 Ko 2,5 s 150 ms
SPA React classique 400 – 1500 Ko 3,2 s 250 ms

Mesures médianes sur des sites de complexité équivalente, 4G simulé.

Erreurs fréquentes en 2026

Mettre 'use client' par habitude (héritage Pages Router)

Les développeurs qui migrent depuis le Pages Router ont le réflexe de mettre 'use client' partout. Résultat : ils perdent tous les bénéfices de l’App Router. Audit rapide : grep -rl "'use client'" app/ | wc -l divisé par le nombre total de composants doit être inférieur à 30 % sur un projet sain.

Importer fs ou process.env.SECRET dans un Client Component

Erreur de build immédiate. Next.js empêche l’import de modules Node serveur dans un fichier 'use client'. Si vous tombez là, c’est que vous mélangez des responsabilités.

Passer une fonction comme prop à un Client Component

Les fonctions ne sont pas sérialisables. Erreur runtime explicite : “Functions cannot be passed directly to Client Components”. Solution : utiliser une Server Action ('use server') qui sera sérialisée par Next.js comme une référence serveur.

Oublier 'use client' sur un fichier qui utilise useState

Erreur de build : “useState only works in Client Components”. Ajoutez la directive en première ligne.

Architecture recommandée pour un projet Next.js 15+

  1. Pages (page.tsx) — Server Component, async, fait les requêtes serveur.
  2. Layouts — Server Components, jamais marqués 'use client' sauf cas exceptionnel.
  3. Composants d’affichage (ProductCard, Article) — Server Components.
  4. Composants interactifs (Modal, Tabs, Counter) — Client Components, isolés en feuilles.
  5. Mutations — Server Actions ('use server'), invoquées depuis les formulaires ou les boutons côté client.
  6. Données dérivées — calculées côté serveur, passées en props sérialisables.

Quand demander de l’aide à un développeur Next.js senior ?

L’arbitrage Server vs Client est l’une des décisions où l’expérience compte le plus. Symptômes qui indiquent qu’il faut faire auditer votre projet :

  • Le bundle app/ dépasse 200 Ko.
  • Plus de 50 % des fichiers contiennent 'use client'.
  • Vous utilisez useEffect + fetch dans 80 % de vos composants.
  • Le LCP mobile dépasse 2,5 s en production.
  • Vous dupliquez la même requête API dans plusieurs composants.

Notre équipe développeur Next.js à Paris intervient typiquement en audit (1-3 jours) ou en refonte d’architecture (5-15 jours) sur ces problématiques précises.

Aller plus loin avec Next.js

Questions fréquentes — Server vs Client Components

Un Server Component peut-il avoir des props événementielles comme onClick ?

Non. Les Server Components sont rendus côté serveur et n’expédient pas de JavaScript au navigateur. Toute interactivité (clic, focus, hover dynamique) doit être confiée à un Client Component. Si vous avez besoin d’un bouton qui déclenche une mutation serveur, utilisez un <form> avec une Server Action.

Les Server Components fonctionnent-ils avec une base SQLite ou MySQL ?

Oui, sans aucune limitation. Vous pouvez importer Prisma, Drizzle, mysql2, postgres ou un client custom directement dans un Server Component et faire des requêtes await db.find(). C’est même le cas d’usage canonique : pas de couche API à maintenir entre le composant et la base.

Puis-je utiliser TanStack Query (React Query) avec les Server Components ?

Oui, mais seulement dans les Client Components. Pour les données initiales serveur, faites un await fetch dans le Server Component et passez les données en props au Client Component, qui les hydratera dans le cache TanStack via HydrationBoundary. Cette approche évite la double requête au mount.

Les Server Components remplacent-ils getServerSideProps ?

Oui, complètement. Un Server Component async avec await fetch() remplace getServerSideProps sans la cérémonie. Bonus : vous pouvez avoir plusieurs requêtes parallèles dans le même fichier sans avoir à fusionner les retours dans un seul objet props.

Comment debug un Server Component qui ne se met pas à jour ?

Trois causes fréquentes : 1) Next.js a mis le résultat en cache (fetch est cached par défaut, utilisez cache: 'no-store' ou revalidate) ; 2) vous lisez des cookies sans headers()/cookies() qui force le rendu dynamique ; 3) le Router Cache ne s’invalide pas — appelez revalidatePath() ou revalidateTag() dans votre Server Action après mutation.

Quelle est la taille typique d’un bundle App Router bien architecturé en 2026 ?

Pour une app de complexité moyenne (10-30 pages, formulaires, dashboard) : 80 à 150 Ko de JS gzippé en first load, dont ~70 Ko viennent du framework Next.js lui-même. C’est 3 à 5 fois moins qu’une SPA équivalente. Si vous dépassez 250 Ko, cherchez les 'use client' mal placés et les bibliothèques tierces lourdes (chart, motion, editor) qui n’ont pas été lazy-loadées.

W
Rédigé par
WebEngine
Développeur web freelance à Paris spécialisé WordPress, WooCommerce et SEO technique depuis 2010. 24 avis vérifiés · Note 5/5. Chaque site livré atteint un score PageSpeed mobile supérieur à 90.

Un projet en tête ?

Devis gratuit sous 48h, sans engagement.

Demander un devis gratuit