Pourquoi les Server Components (et ce qu’ils ne sont PAS)
Ce que les Server Components ne sont PAS :
- ❌ Un remplacement de Next.js API routes
- ❌ Du Server-Side Rendering (SSR)
- ❌ Une nouvelle façon de faire du SSG
Ce qu’ils SONT :
- ✅ Des composants React qui s’exécutent uniquement sur le serveur
- ✅ Pas de JavaScript envoyé au client pour ces composants
- ✅ Accès direct à la base de données, filesystem, etc.
Résultat : bundles JS -40%, performance +30%, meilleur SEO.
Différence entre SSR et Server Components
SSR (Next.js Pages Router)
// pages/users.tsx
export async function getServerSideProps() {"{"}
const users = await db.users.findMany();
return {"{"} props: {"{"} users {"}"} {"}"};
{"}"}
export default function Users({"{"} users {"}"}) {"{"}
return (
<div>
{"{"}users.map(u => <UserCard key={"{"}u.id{"}"} user={"{"}u{"}"} />){"}"}
</div>
);
{"}"}
Problème : <UserCard> est hydraté côté client même s’il est statique → JavaScript inutile
Server Components (App Router)
// app/users/page.tsx
async function Users() {"{"}
const users = await db.users.findMany(); // Direct !
return (
<div>
{"{"}users.map(u => <UserCard key={"{"}u.id{"}"} user={"{"}u{"}"} />){"}"}
</div>
);
{"}"}
Avantage : <UserCard> reste côté serveur → 0 JavaScript envoyé au client
Pattern #1 : Data fetching côté serveur
Avant (Client Component)
'use client';
import {"{"} useEffect, useState {"}"} from 'react';
export default function Dashboard() {"{"}
const [stats, setStats] = useState(null);
useEffect(() => {"{"}
fetch('/api/stats')
.then(r => r.json())
.then(setStats);
{"}"}, []);
if (!stats) return <Loading />;
return <div>{"{"}stats.revenue{"}"}</div>;
{"}"}
Problèmes :
- Waterfall : HTML → JS → API call → data
- Loading state à gérer
- API route nécessaire
Après (Server Component)
// Pas besoin de 'use client'
import {"{"} db {"}"} from '@/lib/db';
async function Dashboard() {"{"}
const stats = await db.stats.findFirst();
return <div>{"{"}stats.revenue{"}"}</div>;
{"}"}
Avantages :
- Pas de loading state
- Pas d’API route
- Plus rapide (1 requête au lieu de 3)
Pattern #2 : Composition Server + Client
Règle : Client Component = dès que vous utilisez hooks ou events
// app/dashboard/page.tsx (Server Component)
import {"{"} db {"}"} from '@/lib/db';
import {"{"} InteractiveChart {"}"} from './InteractiveChart';
async function Dashboard() {"{"}
const data = await db.analytics.findMany();
return (
<div>
<h1>Dashboard</h1>
{"{"}/* Server Component */{"}"}
<StaticStats data={"{"}data{"}"} />
{"{"}/* Client Component (interactif) */{"}"}
<InteractiveChart data={"{"}data{"}"} />
</div>
);
{"}"}
// app/dashboard/InteractiveChart.tsx
'use client'; // Client Component
import {"{"} useState {"}"} from 'react';
import {"{"} Chart {"}"} from 'recharts';
export function InteractiveChart({"{"} data {"}"}) {"{"}
const [timeRange, setTimeRange] = useState('7d');
return (
<div>
<select onChange={"{"}e => setTimeRange(e.target.value){"}"}>
<option value="7d">7 jours</option>
<option value="30d">30 jours</option>
</select>
<Chart data={"{"}data{"}"} range={"{"}timeRange{"}"} />
</div>
);
{"}"}
Astuce : passer data de Server → Client Component est OK (sérialisé automatiquement)
Pattern #3 : Streaming avec Suspense
Problème : page lente si une requête DB est longue
Solution : streamer les parties lentes
// app/dashboard/page.tsx
import {"{"} Suspense {"}"} from 'react';
export default function Dashboard() {"{"}
return (
<div>
<h1>Dashboard</h1>
{"{"}/* Charge immédiatement */{"}"}
<FastStats />
{"{"}/* S'affiche dès que prêt */{"}"}
<Suspense fallback={"{"}< Loading />{"}"}>
<SlowAnalytics />
</Suspense>
<Suspense fallback={"{"}< Loading />{"}"}>
<VerySlowReports />
</Suspense>
</div>
);
{"}"}
async function SlowAnalytics() {"{"}
const analytics = await db.analytics.complex Query(); // 2s
return <AnalyticsChart data={"{"}analytics{"}"} />;
{"}"}
Résultat :
- TTFB (Time to First Byte) : < 100ms
- Contenu s’affiche progressivement
- Meilleur UX
Pattern #4 : Cache et revalidation
Cache automatique
// Par défaut, fetch() est cacché
async function Posts() {"{"}
const posts = await fetch('https://api.blog.com/posts'); // Cacché ∞
return <PostList posts={"{"}posts{"}"} />;
{"}"}
Revalidation périodique
async function Posts() {"{"}
const posts = await fetch('https://api.blog.com/posts', {"{"}
next: {"{"} revalidate: 60 {"}"} // Re-fetch toutes les 60s
{"}"});
return <PostList posts={"{"}posts{"}"} />;
{"}"}
Revalidation on-demand
// app/api/revalidate/route.ts
import {"{"} revalidatePath {"}"} from 'next/cache';
export async function POST() {"{"}
revalidatePath('/posts'); // Invalide le cache de /posts
return Response.json({"{"} revalidated: true {"}"});
{"}"}
Trigger : webhook depuis CMS, cron job, etc.
Pattern #5 : Parallel data fetching
❌ Waterfall (lent)
async function Dashboard() {"{"}
const user = await db.user.findFirst();
const posts = await db.posts.findMany({"{"} where: {"{"} userId: user.id {"}"} {"}"});
const comments = await db.comments.findMany({"{"} where: {"{"} userId: user.id {"}"} {"}"});
return ...
{"}"}
// user (200ms) → posts (150ms) → comments (100ms) = 450ms total
✅ Parallèle (rapide)
async function Dashboard() {"{"}
const [user, posts, comments] = await Promise.all([
db.user.findFirst(),
db.posts.findMany(),
db.comments.findMany()
]);
return ...
{"}"}
// max(200ms, 150ms, 100ms) = 200ms total
Pièges courants
Piège #1 : Passer des fonctions à Client Components
❌ Ne fonctionne pas
// Server Component
async function Page() {"{"}
const handleClick = () => console.log('clicked');
return <Button onClick={"{"}handleClick{"}"} />; // Erreur !
{"}"}
Raison : fonctions non sérialisables
✅ Solution
// app/page.tsx (Server)
export default function Page() {"{"}
return <Button />;
{"}"}
// app/Button.tsx (Client)
'use client';
export function Button() {"{"}
const handleClick = () => console.log('clicked');
return <button onClick={"{"}handleClick{"}"}>Click</button>;
{"}"}
Piège #2 : Importer Server Component dans Client Component
❌ Ne fonctionne pas
// ClientComponent.tsx
'use client';
import {"{"} ServerComponent {"}"} from './ServerComponent'; // Erreur !
export function ClientComponent() {"{"}
return <ServerComponent />;
{"}"}
✅ Solution : passer en children
// page.tsx (Server)
export default function Page() {"{"}
return (
<ClientComponent>
<ServerComponent /> {"{"}/* OK */{"}"}
</ClientComponent>
);
{"}"}
// ClientComponent.tsx
'use client';
export function ClientComponent({"{"} children {"}"}) {"{"}
return <div>{"{"}children{"}"}</div>;
{"}"}
Piège #3 : Context providers
❌ Problème
// layout.tsx
export default function RootLayout({"{"} children {"}"}) {"{"}
return (
<ThemeProvider> {"{"}/* Erreur : Context = Client Component */{"}"}
{"{"}children{"}"}
</ThemeProvider>
);
{"}"}
✅ Solution : wrapper client
// app/providers.tsx
'use client';
export function Providers({"{"} children {"}"}) {"{"}
return (
<ThemeProvider>
<AuthProvider>
{"{"}children{"}"}
</AuthProvider>
</ThemeProvider>
);
{"}"}
// layout.tsx (Server)
import {"{"} Providers {"}"} from './providers';
export default function RootLayout({"{"} children {"}"}) {"{"}
return (
<html>
<body>
<Providers>
{"{"}children{"}"}
</Providers>
</body>
</html>
);
{"}"}
Migration depuis Pages Router
Étape 1 : Identifier les composants
// pages/dashboard.tsx (avant)
export default function Dashboard({"{"} data {"}"}) {"{"}
const [filter, setFilter] = useState('all');
return (
<div>
<Stats data={"{"}data{"}"} /> {"{"}/* Peut être Server Component */{"}"}
<Filter value={"{"}filter{"}"} onChange={"{"}setFilter{"}"} /> {"{"}/* Client */{"}"}
<Table data={"{"}data{"}"} filter={"{"}filter{"}"} /> {"{"}/* Client */{"}"}
</div>
);
{"}"}
Étape 2 : Séparer Server/Client
// app/dashboard/page.tsx (Server Component)
async function Dashboard() {"{"}
const data = await db.getData();
return (
<div>
<Stats data={"{"}data{"}"} /> {"{"}/* Server Component */{"}"}
<DashboardClient data={"{"}data{"}"} />
</div>
);
{"}"}
// app/dashboard/DashboardClient.tsx
'use client';
export function DashboardClient({"{"} data {"}"}) {"{"}
const [filter, setFilter] = useState('all');
return (
<>
<Filter value={"{"}filter{"}"} onChange={"{"}setFilter{"}"} />
<Table data={"{"}data{"}"} filter={"{"}filter{"}"} />
</>
);
{"}"}
Étape 3 : Supprimer getServerSideProps
Avant
export async function getServerSideProps() {"{"}
const data = await fetch('...');
return {"{"} props: {"{"} data {"}"} {"}"};
{"}"}
Après
async function Page() {"{"}
const data = await fetch('...'); // Direct dans le composant
return ...
{"}"}
Checklist de migration
Analyse
[ ] Identifier composants interactifs (Client)
[ ] Identifier composants statiques (Server)
[ ] Lister les Context providers
Migration
[ ] Créer app/ directory
[ ] Migrer layouts
[ ] Convertir pages en Server Components
[ ] Extraire Client Components
[ ] Wrapper les providers
Optimisation
[ ] Ajouter Suspense boundaries
[ ] Paralléliser data fetching
[ ] Configurer cache/revalidate
Validation
[ ] Tests passent
[ ] Bundle size réduit
[ ] Lighthouse score amélioré
Gains réels
Projet Next.js 13 → 14 (App Router + RSC)
Avant
- Bundle JS : 280KB
- LCP : 2.4s
- PageSpeed : 67/100
Après
- Bundle JS : 140KB (-50%)
- LCP : 1.1s (-54%)
- PageSpeed : 92/100
Impact business : taux de rebond -22%, conversions +18%
Questions sur les React Server Components ? Contactez-moi pour un audit de votre app Next.js.