React Server Components vs Client-side React (SPA) Comparaison

Une couche React pré-rendue côté serveur, zéro JS par défaut

VS
Client-side React (SPA)

Un mode d'usage classique et mature de React, rendu dans le navigateur

18 min de lectureFrontend

Verdict rapide

Sur les surfaces à contenu dense et critiques pour le SEO, RSC apporte un gain mesurable : dans l'exemple de react.dev, une bibliothèque de 75 Ko gzippés ne part jamais vers le client. Sur les panneaux internes fortement interactifs (dashboard, admin), le client-first offre à la fois plus de simplicité et une surface d'erreur réduite. L'avertissement critique est réel : passer un objet complet du Server au Client en tant que prop écrit tous les champs de cet objet dans le flight payload ; si la frontière est mal tracée, le gain s'érode silencieusement. Prenez la décision par la mesure, pas par la mode.

React Server ComponentsClient-side React (SPA)
Lire le verdict complet

Comparaison des scores

Chargement du graphique...

Notation détaillée

Notation détaillée: React Server Components et Client-side React (SPA) — notes sur 10, catégorie par catégorie
CatégorieReact Server ComponentsClient-side React (SPA)
Performance
8/10
6/10
Facilité d'apprentissage
4/10
8/10
Écosystème
7/10
9/10
Communauté
7/10
9/10
Marché de l'emploi
7/10
8/10
Pérennité
9/10
7/10

Avantages & Inconvénients

React Server Components

Avantages

  • N'introduit jamais les bibliothèques lourdes (parseur markdown, sanitizer) dans le bundle client sur les pages à contenu dense — 75 Ko gzippés économisés dans l'exemple officiel de react.dev
  • La récupération de données côté serveur élimine la cascade client-serveur (ex. fetch séquentiel Note→Author)
  • Le HTML est visible instantanément au premier chargement, le contenu est lisible sans attendre l'hydratation JS
  • Intégration native à la couche ISR/cache du Next.js App Router — service instantané via stale-while-revalidate
  • Avec React 19.2 Activity et Next.js 16.3 Instant Navigations, la perte de sensation SPA est largement comblée
  • La fusion des chunk-groups de Turbopack optimise automatiquement le bundle client

Inconvénients

  • Chaque prop traversant la frontière Server→Client doit être sérialisable ; si une valeur non prise en charge est transmise, React lève une exception
  • Quand 'use client' marque un fichier, TOUS ses imports transitifs sont inclus dans le bundle client — mal tracer la frontière annule silencieusement le gain
  • Un Server Component ne peut pas créer de contexte ni utiliser d'API interactives comme useState/useEffect
  • Les API sous-jacentes de RSC ne suivent pas le semver entre versions mineures de React — il faut épingler une version spécifique du bundler/framework
  • Une faille critique de RCE non authentifiée a été découverte le 3 décembre 2025 (corrigée par 19.0.1/19.1.2/19.2.1) — la surface de sécurité dépendante du framework s'accroît
  • Courbe d'apprentissage raide : tracer correctement la frontière serveur/client demande de l'expérience, les messages d'erreur apparaissent souvent à l'exécution

Idéal pour

Pages à contenu dense, critiques pour le SEO (blog, produit, documentation, pages de comparaison)Écrans à forte charge de premier rendu, proches de la source de données, récupérables en une seule fois côté serveurPages à fort trafic mais peu changeantes, alimentées par ISR/cacheÉquipes déjà engagées sur le Next.js App Router et souhaitant une architecture native au frameworkProjets devant réduire de façon mesurable la taille du bundle JS client

Client-side React (SPA)

Avantages

  • Modèle mental simple : chaque composant peut toujours être interactif, aucune frontière serveur/client à penser
  • Avec un outillage basé sur Vite : dev-server rapide, HMR et code-splitting mature (Rollup)
  • Aucune frontière de sérialisation — l'état vit directement comme objet JS dans le navigateur, pas de piège de 'flight payload'
  • L'écosystème React le plus vaste et le plus ancien ; le plus large vivier de bibliothèques, de réponses Stack Overflow et de recrutement
  • Aucune obligation de faire tourner un serveur — déployable sur un hébergement statique/CDN
  • Le débogage se fait dans un seul environnement (DevTools du navigateur), pas de double surface d'erreur serveur/client

Inconvénients

  • Le premier chargement est généralement un `<div id="root">` vide + un bundle JS — le TTI et le FCP convergent, aucun contenu n'apparaît sans l'exécution du JS
  • Les bibliothèques lourdes (parseur markdown, sanitizer, bibliothèque de graphiques) entrent toujours dans le bundle client, aucune option pour les laisser côté serveur
  • Si la récupération de données glisse vers des chaînes de `useEffect` séquentielles, le risque de cascade client-serveur est élevé
  • Les stratégies chunk unique / chunk par page peuvent faire retélécharger le code partagé de façon répétée — le problème que Turbopack cherche à résoudre reste, côté SPA, à la charge de l'outillage (Vite/Rollup)
  • Pour le SEO et la performance du premier chargement, il faut mettre en place une couche SSG/prerender séparée, le framework lui-même ne la fournit pas

Idéal pour

Panneaux internes fortement interactifs (admin, dashboard) — changements d'état fréquents, glisser-déposer, filtrage en directApplications authentifiées où le SEO n'est pas une prioritéPrototypage rapide et développement de MVP par de petites équipesProjets destinés à un hébergement statique/CDN, sans coût de serveur à faire tournerMaintenance et modernisation progressive de grandes bases de code SPA existantes

Comparaison de code

React Server Components
// Next.js App Router — Server Component (par défaut)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component (dans un fichier séparé avec 'use client')

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;

  // await direct côté serveur — pas de cascade client-serveur,
  // des bibliothèques lourdes comme marked/sanitize-html n'entrent jamais dans le bundle client
  const post = await db.post.findUnique({ where: { slug } });
  if (!post) return <div>Introuvable</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {/* Seules les props sérialisables sont transmises au Client Component */}
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <LikeButton postId={post.id} initialCount={post.likeCount} />
    </article>
  );
}

// app/posts/[slug]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';

export default function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => {
        setCount((c) => c + 1);
        await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
      })}
    >
      J'aime ({count})
    </button>
  );
}
Client-side React (SPA)
// Vite + React — Client-side SPA
// src/pages/PostPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';

interface Post {
  id: string;
  title: string;
  renderedHtml: string;
  likeCount: number;
}

export default function PostPage() {
  const { slug } = useParams();
  const [post, setPost] = useState<Post | null>(null);
  const [isLiking, setIsLiking] = useState(false);

  useEffect(() => {
    // Toute la récupération de données se fait côté client — le risque de cascade commence ici
    let cancelled = false;
    fetch(`/api/posts/${slug}`)
      .then((res) => res.json())
      .then((data) => { if (!cancelled) setPost(data); });
    return () => { cancelled = true; };
  }, [slug]);

  if (!post) return <div>Chargement...</div>; // Cet écran n'apparaît jamais sans l'exécution du JS

  async function handleLike() {
    setIsLiking(true);
    setPost((p) => (p ? { ...p, likeCount: p.likeCount + 1 } : p));
    await fetch(`/api/posts/${post!.id}/like`, { method: 'POST' });
    setIsLiking(false);
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <button disabled={isLiking} onClick={handleLike}>
        J'aime ({post.likeCount})
      </button>
    </article>
  );
}

// vite.config.ts — le code-splitting est délégué à Rollup
export default {
  build: {
    rollupOptions: {
      output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
    },
  },
};

Conclusion

Sur les surfaces à contenu dense et critiques pour le SEO, RSC apporte un gain mesurable : dans l'exemple de react.dev, une bibliothèque de 75 Ko gzippés ne part jamais vers le client. Sur les panneaux internes fortement interactifs (dashboard, admin), le client-first offre à la fois plus de simplicité et une surface d'erreur réduite. L'avertissement critique est réel : passer un objet complet du Server au Client en tant que prop écrit tous les champs de cet objet dans le flight payload ; si la frontière est mal tracée, le gain s'érode silencieusement. Prenez la décision par la mesure, pas par la mode.

Obtenir une consultation gratuite
FAQ

Questions fréquentes

Sur les surfaces à contenu dense, critiques pour le SEO, où le chargement initial (LCP/TTI) compte (blog, page produit, page de comparaison), RSC apporte un gain mesurable — récupération de données côté serveur et moins de JS client. Selon les propres données de Next.js 16.3, le passage aux flux natifs de Node.js pour le rendu côté serveur permet de traiter 22 % de requêtes en plus sous charge. Sur les panneaux internes fortement interactifs (dashboard, admin), le client-first offre généralement une expérience de développement plus simple.

Articles de blog associés

Voir tous les articles
Toutes les comparaisons