Pourquoi les React Server Components méritent votre attention

Pendant longtemps, développer une application React signifiait principalement envoyer du JavaScript au navigateur, puis laisser ce JavaScript reconstruire l’interface, gérer l’état, déclencher les appels API et brancher les interactions. Cette approche reste puissante, mais elle peut devenir coûteuse : bundles volumineux, hydratation lente, appels réseau dispersés et logique métier trop proche de l’interface.

Les React Server Components, souvent abrégés en RSC, proposent une autre répartition du travail. Certains composants s’exécutent uniquement côté serveur. Ils peuvent lire des fichiers, interroger une base de données, appeler une API interne ou préparer des données sans jamais être envoyés dans le bundle JavaScript du navigateur.

Ce n’est pas simplement une optimisation de performance. C’est un changement d’architecture. Comme pour le typage des données API en TypeScript, l’objectif est de réduire la fragilité en plaçant la bonne responsabilité au bon endroit.

Le problème classique : tout finit côté client

Prenons une page de catalogue produits. Dans une application React traditionnelle, on peut rapidement obtenir ce type de composant :

'use client'

import { useEffect, useState } from 'react'

type Product = {
  id: string
  name: string
  price: number
}

export function ProductList() {
  const [products, setProducts] = useState<Product[]>([])
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    fetch('/api/products')
      .then((response) => response.json())
      .then((data) => setProducts(data))
      .finally(() => setIsLoading(false))
  }, [])

  if (isLoading) {
    return <p>Chargement...</p>
  }

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} — {product.price} €
        </li>
      ))}
    </ul>
  )
}

Ce code fonctionne, mais il cumule plusieurs responsabilités côté navigateur : chargement des données, état de chargement, parsing JSON, rendu initial vide, puis mise à jour après l’appel API. Sur une interface simple, ce n’est pas dramatique. Sur une application complète, cela augmente la quantité de JavaScript exécutée sur le thread principal, un sujet proche des problématiques abordées dans l’article sur les Web Workers en JavaScript.

Le principe : un composant peut rester côté serveur

Avec les React Server Components, un composant peut être asynchrone et récupérer ses données directement côté serveur :

type Product = {
  id: string
  name: string
  price: number
}

async function getProducts(): Promise<Product[]> {
  const response = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }
  })

  if (!response.ok) {
    throw new Error('Impossible de charger les produits')
  }

  return response.json()
}

export default async function ProductList() {
  const products = await getProducts()

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} — {product.price} €
        </li>
      ))}
    </ul>
  )
}

Ici, le composant ne contient ni useEffect, ni useState, ni logique de chargement côté client. Il s’exécute sur le serveur, produit une représentation de l’interface, puis transmet au navigateur uniquement ce qui est nécessaire.

Dans un framework comme Next.js avec l’App Router, les composants sont des Server Components par défaut. Il faut ajouter explicitement 'use client' lorsqu’un composant a besoin d’interactivité côté navigateur.

Server Component ou Client Component ?

La règle de base est simple : commencez côté serveur, puis passez côté client uniquement quand c’est nécessaire.

Un Server Component est adapté pour :

  • charger des données ;
  • accéder à une base de données ;
  • lire des variables d’environnement privées ;
  • générer une structure HTML stable ;
  • composer une page à partir de contenu distant ;
  • réduire le JavaScript envoyé au navigateur.

Un Client Component est nécessaire pour :

  • utiliser useState, useEffect, [useReducer](/articles/machines-etats-typescript) ou d’autres hooks client ;
  • réagir aux événements utilisateur comme onClick ou onChange ;
  • accéder à window, document, localStorage ou aux API navigateur ;
  • manipuler une animation interactive ;
  • gérer un formulaire dynamique côté client.

Par exemple, une page produit peut rester principalement serveur, tout en déléguant le bouton d’ajout au panier à un composant client :

// app/products/[id]/page.tsx
import { AddToCartButton } from './AddToCartButton'

async function getProduct(id: string) {
  const response = await fetch(`https://api.example.com/products/${id}`)
  return response.json()
}

type PageProps = {
  params: Promise<{ id: string }>
}

export default async function ProductPage({ params }: PageProps) {
  const { id } = await params
  const product = await getProduct(id)

  return (
    <article>
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <strong>{product.price} €</strong>
      <AddToCartButton productId={product.id} />
    </article>
  )
}
// app/products/[id]/AddToCartButton.tsx
'use client'

import { useState } from 'react'

type AddToCartButtonProps = {
  productId: string
}

export function AddToCartButton({ productId }: AddToCartButtonProps) {
  const [isAdded, setIsAdded] = useState(false)

  function addToCart() {
    setIsAdded(true)
    console.log(`Produit ajouté : ${productId}`)
  }

  return (
    <button type="button" onClick={addToCart} disabled={isAdded}>
      {isAdded ? 'Ajouté' : 'Ajouter au panier'}
    </button>
  )
}

La page charge les données côté serveur. Le bouton, lui, reste interactif côté client. Cette séparation évite de transformer toute la page en composant client simplement parce qu’un élément a besoin d’un onClick.

Attention aux frontières entre serveur et client

La frontière entre Server Component et Client Component impose une contrainte importante : les props passées du serveur vers le client doivent être sérialisables. En pratique, vous pouvez transmettre des chaînes, nombres, booléens, tableaux et objets simples. En revanche, vous ne pouvez pas passer directement une fonction, une connexion à une base de données ou une instance de classe complexe.

Ce code est problématique :

import { LikeButton } from './LikeButton'

export default function ArticleActions() {
  function handleLike() {
    console.log('like')
  }

  return <LikeButton onLike={handleLike} />
}

À la place, il faut déplacer la logique interactive dans le composant client, ou utiliser une action serveur quand le framework le permet.

'use client'

export function LikeButton() {
  function handleLike() {
    console.log('like')
  }

  return (
    <button type="button" onClick={handleLike}>
      Aimer
    </button>
  )
}

Cette frontière oblige à mieux penser la composition. Elle pousse à distinguer les composants de données, les composants de présentation et les composants interactifs.

Une architecture de dossier plus lisible

Pour éviter les confusions, il est utile d’adopter une convention claire :

app/
  products/
    page.tsx
    ProductCard.tsx
    ProductFilters.client.tsx
    AddToCartButton.client.tsx
lib/
  products.ts

Le suffixe .client.tsx n’est pas obligatoire, mais il rend la lecture immédiate. On comprend rapidement quels composants sont envoyés au navigateur.

Le fichier lib/products.ts peut contenir les fonctions d’accès aux données :

export type Product = {
  id: string
  name: string
  price: number
  category: string
}

export async function getProducts(): Promise<Product[]> {
  const response = await fetch('https://api.example.com/products')

  if (!response.ok) {
    throw new Error('Erreur lors du chargement des produits')
  }

  return response.json()
}

Cette organisation fonctionne particulièrement bien avec une démarche de design system. Les composants purement visuels peuvent rester côté serveur tant qu’ils n’ont pas besoin d’état local. Les composants liés aux thèmes, variantes et tokens peuvent s’inscrire dans une architecture proche de celle présentée dans Design tokens en CSS et TypeScript.

Performance : le gain vient surtout du JavaScript évité

Les React Server Components ne rendent pas automatiquement une application rapide. Ils réduisent surtout une source majeure de coût : le JavaScript client inutile.

Moins de JavaScript côté navigateur signifie généralement :

  • moins de code à télécharger ;
  • moins de code à parser ;
  • moins de travail d’hydratation ;
  • moins de risques de bloquer l’interaction utilisateur ;
  • une meilleure séparation entre contenu statique et zones interactives.

Cela complète d’autres optimisations frontend, comme l’optimisation des médias décrite dans Optimiser les images web sans sacrifier la qualité. Une page peut avoir un excellent modèle de rendu et rester lente si elle charge des images trop lourdes, des polices mal configurées ou des scripts tiers excessifs.

Les erreurs fréquentes à éviter

La première erreur consiste à mettre 'use client' trop haut dans l’arbre. Si vous l’ajoutez dans un layout global ou un composant parent très large, tous ses descendants risquent de basculer dans le monde client. Vous perdez alors une grande partie du bénéfice architectural.

La deuxième erreur est de dupliquer la logique de récupération des données : une fois côté serveur, puis une seconde fois côté client. Cela crée des incohérences, complexifie le cache et rend les états de chargement plus difficiles à maîtriser.

La troisième erreur est de considérer les Server Components comme un remplacement de l’accessibilité, du HTML sémantique ou de la qualité CSS. Le rendu serveur ne corrige pas un mauvais balisage. Pour les interfaces de saisie, les principes vus dans Formulaires accessibles : concevoir des interfaces web vraiment utilisables restent indispensables.

Conclusion

Les React Server Components invitent à revenir à une question fondamentale : quel code doit vraiment s’exécuter dans le navigateur ?

Pour les contenus, les pages de détail, les listes, les tableaux de bord partiellement statiques et les interfaces fortement liées aux données, ils permettent de réduire la surface JavaScript client et de rendre l’architecture plus explicite. Pour les interactions riches, les composants client restent nécessaires.

La bonne approche n’est donc pas d’opposer serveur et client, mais de composer les deux intelligemment. Gardez les données et le rendu stable côté serveur. Réservez le client aux interactions qui en ont réellement besoin. Vous obtiendrez des applications React plus légères, plus lisibles et souvent plus faciles à maintenir.