Chapitre 13
Chapitre 13 — SSR (Server-Side Rendering)
Chapitre 13 — SSR (Server-Side Rendering)
SSR (Server-Side Rendering) — Cours complet
1. Le problème des SPAs
CSR (Client-Side Rendering) classique
<div id="root"></div>
<script src="/bundle.js"></script>
Le navigateur télécharge un HTML quasi-vide, puis exécute le JS pour tout générer.
Problèmes :
- SEO : les robots crawlers (Googlebot) exécutent du JS, mais partiellement. Les réseaux sociaux (Open Graph, Twitter Cards) ne l'exécutent pas du tout.
- FCP (First Contentful Paint) retardé : le navigateur doit télécharger, parser et exécuter le JS avant d'afficher quoi que ce soit.
- Bundle size : plus l'application grossit, plus le JS téléchargé est lourd.
- Perceived performance : écran blanc pendant le chargement.
Métriques critiques
| Métrique | CSR (mauvais réseau) | SSR | Objectif |
|---|---|---|---|
| FCP | 3-6s | 1-2s | < 1.8s |
| LCP | 5-10s | 2-4s | < 2.5s |
| TTFB | 0.3s | 0.5-1s | < 0.8s |
| TBT (Total Blocking Time) | 500ms+ | 100-300ms | < 200ms |
| SI (Speed Index) | 5-8s | 2-3s | < 3s |
2. Stratégies de rendu
CSR (Client-Side Rendering)
- Tout le rendu côté client.
- Use case : dashboard internes, apps authentifiées.
- Avantage : interactions riches sans rechargement.
- Inconvénient : FCP lent, SEO limité.
SSR (Server-Side Rendering)
- HTML généré sur le serveur à chaque requête.
- Use case : pages dynamiques, e-commerce, contenus personnalisés.
- Avantage : SEO, FCP rapide.
- Inconvénient : TTFB plus élevé, coût serveur.
SSG (Static Site Generation)
- HTML généré au build time.
- Use case : blogs, documentation, landing pages.
- Avantage : ultra-rapide, pas de serveur nécessaire.
- Inconvénient : pas de contenu dynamique sans re-build.
ISR (Incremental Static Regeneration)
- SSG avec revalidation au runtime.
- Use case : pages semi-dynamiques (blog avec mises à jour fréquentes).
- Avantage : vitesse du statique + fraîcheur du dynamique.
RSC (React Server Components)
- Composants React exécutés uniquement côté serveur.
- Use case : composants lourds (dates, markdown, accès DB).
- Avantage : zéro JS envoyé au client.
- Inconvénient : complexité architecture.
Tableau comparatif
| Stratégie | Build | Runtime | SEO | TTFB | Fraîcheur |
|---|---|---|---|---|---|
| CSR | Non | Client | ❌ | ✅ | ✅ |
| SSR | Non | Serveur | ✅ | ❌ | ✅ |
| SSG | Oui | Aucun | ✅ | ✅ | ❌ |
| ISR | Oui | Serveur (revalidate) | ✅ | ✅ | ⚠️ |
| RSC | Non | Serveur | ✅ | ⚠️ | ✅ |
3. Next.js App Router
Structure
app/
page.tsx → GET /
layout.tsx → Layout racine
loading.tsx → Loading state
error.tsx → Error boundary
not-found.tsx → 404
blog/
page.tsx → GET /blog
[slug]/
page.tsx → GET /blog/:slug
api/
route.tsx → Route handler
Server Components (par défaut)
// app/page.tsx — Server Component (par défaut)
export default async function Home() {
const posts = await db.post.findMany(); // Accès direct à la DB
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
Client Components
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
Règle : tout ce qui n'a pas besoin d'interactivité côté client doit être Server Component.
Server Actions
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
revalidatePath('/blog');
}
// app/blog/new/page.tsx
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Publier</button>
</form>
);
}
Avantages : pas d'API route à créer, progression enhancement, sécurité (jamais exposé au client).
4. Remix
Nested Routes
app/
routes/
_index.tsx → /
blog.tsx → Layout /blog
blog.$slug.tsx → /blog/:slug
blog.new.tsx → /blog/new
Loaders et Actions
// app/routes/blog.$slug.tsx
import { json } from '@remix-run/node';
import { useLoaderData, useFetcher } from '@remix-run/react';
import { db } from '~/db';
export async function loader({ params }: LoaderArgs) {
const post = await db.post.findUnique({
where: { slug: params.slug },
});
if (!post) throw new Response('Not Found', { status: 404 });
return json(post, {
headers: {
'Cache-Control': 's-maxage=60, stale-while-revalidate=120',
},
});
}
export async function action({ request, params }: ActionArgs) {
const formData = await request.formData();
const intent = formData.get('intent');
if (intent === 'like') {
await db.post.update({
where: { slug: params.slug },
data: { likes: { increment: 1 } },
});
return json({ ok: true });
}
}
export default function Post() {
const post = useLoaderData<typeof loader>();
const fetcher = useFetcher();
return (
<div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<fetcher.Form method="post">
<button name="intent" value="like">
Likes: {post.likes}
</button>
</fetcher.Form>
</div>
);
}
Concepts clés Remix :
- loader : chargement des données côté serveur (GET).
- action : mutations côté serveur (POST, PUT, PATCH, DELETE).
- useFetcher : mutations sans navigation.
- Nested routes : chaque route peut avoir ses propres données et son propre layout.
5. Hydratation
Comment ça marche
- Le serveur génère le HTML (contient le contenu + les data attributes).
- Le navigateur affiche le HTML immédiatement (FCP rapide).
- Le JS React se télécharge en arrière-plan.
- React "hydrate" le DOM : attache les event listeners, initialise les hooks, sans re-rendre le contenu existant.
Problèmes
- Tout ou rien : on hydrate l'arbre entier, même ce qui n'a pas besoin d'interactivité.
- Coût : l'hydratation bloque le thread principal (TBT élevé).
- Mismatch : si le HTML serveur diffère du render client (ex : date, random), React jette tout et re-rend → flash.
- Double payload : le HTML + le JS sont tous deux envoyés.
Solutions
- Selective Hydration (React 18) : hydrater par morceaux avec Suspense.
- Partial Hydration (Astro, Qwik) : n'hydrater que les composants interactifs.
- Resumability (Qwik) : pas d'hydratation, on reprend l'exécution là où le serveur s'est arrêté.
6. Streaming SSR et Suspense
React 18 permet de streamer le HTML au fur et à mesure de sa génération.
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Chargement...</div>}>
<SlowComponent />
</Suspense>
<Suspense fallback={<Skeleton />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
Avantages du streaming :
- TTFB amélioré : le serveur envoie le début du HTML avant d'avoir terminé.
- Progressive rendering : l'utilisateur voit le contenu au fur et à mesure.
- Suspense boundaries : chaque composant peut être chargé indépendamment.
Dans Next.js App Router, le streaming est activé par défaut avec loading.tsx.
7. React Server Components (RSC)
Architecture
┌─────────────┐ ┌──────────────┐
│ Serveur │────→│ Client │
│ │ │ │
│ RSC Payload │ │ Client │
│ (sérialisé) │ │ Components │
└─────────────┘ └──────────────┘
- Les Server Components ne sont jamais inclus dans le bundle JS client.
- Ils produisent un RSC Payload (format sérialisé) qui contient le résultat du rendu.
- Les Client Components peuvent importer des Server Components via
children(composition).
Avantages
- Zéro JS pour les composants purement serveur.
- Accès direct aux données (DB, filesystem, API).
- Réduction drastique du bundle JS.
- Meilleure sécurité (logique métier jamais exposée).
Quand utiliser Server Component ?
| ✅ Server Component | ❌ Client Component |
|---|---|
| Affichage de données | useState, useEffect |
| Markdown rendering | onClick, onChange |
| Date formatting | useReducer, useContext |
| Accès DB | window, document |
| Calculs lourds | Animations JS |
| SEO-critical content | Formulaires complexes |
8. Static Site Generation (SSG)
Quand utiliser SSG ?
- Pages qui ne changent pas souvent.
- Blogs, documentation, landing pages.
- Contenu headless CMS (Contentful, Strapi).
// Next.js App Router — par défaut (statique)
export default async function About() {
const content = await getContent(); // build time
return <div>{content}</div>;
}
Pour forcer le dynamique :
export const dynamic = 'force-dynamic';
// ou
export const revalidate = 3600; // ISR
Avantages SSG
- Site entièrement statique → hébergement CDN (pas de serveur).
- TTFB quasi nul.
- Résilient (pas de dépendance runtime).
Inconvénients SSG
- Build time augmente avec le nombre de pages.
- Contenu figé jusqu'au prochain build.
- Pas adapté aux données personnalisées par utilisateur.
9. ISR (Incremental Static Regeneration)
Combine les avantages du statique (vitesse) et du dynamique (fraîcheur).
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // secondes
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map(p => ({ slug: p.slug }));
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div>{post.content}</div>
</article>
);
}
Fonctionnement :
- Première visite → page générée et mise en cache.
- Requêtes suivantes → page servie depuis le cache (pendant 3600s).
- Après 3600s → la page est régénérée en arrière-plan (stale-while-revalidate).
- Tant que la régénération n'est pas terminée → l'ancienne version est servie.
ISR On-Demand
import { revalidatePath } from 'next/cache';
// Après une mutation CMS
export async function POST(request: Request) {
const { slug } = await request.json();
revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true });
}
10. Edge Runtime vs Node.js Runtime
| Critère | Node.js Runtime | Edge Runtime |
|---|---|---|
| Environnement | Serveur Node | V8 isolates (Cloudflare Workers, Vercel Edge) |
| API disponibles | Toutes | Web APIs (fetch, Request, Response, crypto) |
| Accès filesystem | ✅ | ❌ |
| Accès DB direct | ✅ | ❌ (via HTTP) |
| Démarrage à froid | 200-500ms | < 10ms |
| Régions | Limitée | Globale |
| Bundle size | Illimité | < 1MB (Vercel) |
| Coût | Variable | Faible |
// Edge Runtime : app/page.tsx
export const runtime = 'edge';
export default async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>;
}
// Node.js Runtime : app/page.tsx
export const runtime = 'nodejs';
export default async function Page() {
const data = await db.query('SELECT * FROM posts');
return <div>{data.title}</div>;
}
11. Comparaison des frameworks
Next.js
- Rendu : SSR, SSG, ISR, RSC
- Runtime : Node.js, Edge
- Points forts : Écosystème React, App Router, maturité
- Points faibles : Bundle parfois lourd, complexité App Router
Remix
- Rendu : SSR uniquement (pas de SSG natif)
- Runtime : Node.js
- Points forts : Nested routes, mutations natives, progressive enhancement
- Points faibles : Communauté plus petite, moins d'outils
Astro
- Rendu : SSG, SSR (optionnel)
- Runtime : Node.js, Edge (via adapter)
- Points forts : Zero JS par défaut, partial hydration, multi-framework
- Points faibles : Pas de SPA, interactivité limitée sans framework
Qwik
- Rendu : SSR avec resumability
- Runtime : Node.js, Edge
- Points forts : Quasi zéro JS, résumabilité (pas d'hydratation)
- Points faibles : Écosystème jeune, courbe d'apprentissage
12. Performance SSR
TTFB (Time To First Byte)
Facteurs influençant le TTFB :
- Temps de rendu serveur (composants lourds, accès DB).
- Distance géographique (latence réseau).
- Runtime (Node.js vs Edge).
- Cache (CDN, full route cache).
Optimisations :
- Streaming : envoyer le HTML dès que possible.
- Cache :
stale-while-revalidatepour les pages non-personnalisées. - Edge : déploiement global pour réduire la latence.
- Server Components : réduire le travail serveur en déléguant au client ce qui peut l'être.
Cache
// Cache HTTP
export async function loader() {
return json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
});
}
- CDN Cache : pages statiques et ISR.
- Full Route Cache (Next.js) : cache du rendu RSC.
- Data Cache (Next.js) : cache des fetch requests.
- Router Cache (client) : préchargement des pages visitées.
13. Déploiement SSR
Vercel
- Optimisé pour Next.js.
- Edge Functions disponibles.
- Analytics intégrés.
Cloudflare Pages + Workers
- Edge Runtime natif.
- Workers pour le rendu SSR.
- Bindings (KV, D1, R2).
AWS (Lambda + CloudFront)
- Node.js runtime via Lambda@Edge ou Lambda + API Gateway.
- Contrôle total de l'infrastructure.
- Plus complexe à configurer.
14. Bonnes pratiques SSR
- Préférer Server Components sauf si interactivité nécessaire.
- Utiliser Suspense pour le streaming et les charges lentes.
- Configurer le cache à tous les niveaux (CDN, route, data).
- Éviter les calculs lourds dans le rendu serveur.
- Stratégie de revalidation adaptée (ISR > SSR pur si possible).
- Surveiller le TTFB comme métrique clé.
- Minimiser l'hydratation : moins de Client Components = meilleur TBT.
- Error Boundaries côté serveur et côté client.
15. Conclusion
Le SSR moderne n'est plus une simple option SEO — c'est une architecture complète qui influence la structure des composants, le déploiement, le cache et la performance perçue. Le choix entre Next.js, Remix, Astro ou Qwik dépend du besoin : applications complexes (Next.js, Remix), sites de contenu (Astro), performance ultime (Qwik). La maîtrise des concepts de rendu (SSR, SSG, ISR, RSC) est aujourd'hui une compétence fondamentale pour tout développeur front-end senior.