MFormations
Modern Frontend Engineering

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étriqueCSR (mauvais réseau)SSRObjectif
FCP3-6s1-2s< 1.8s
LCP5-10s2-4s< 2.5s
TTFB0.3s0.5-1s< 0.8s
TBT (Total Blocking Time)500ms+100-300ms< 200ms
SI (Speed Index)5-8s2-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égieBuildRuntimeSEOTTFBFraîcheur
CSRNonClient
SSRNonServeur
SSGOuiAucun
ISROuiServeur (revalidate)⚠️
RSCNonServeur⚠️

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

  1. Le serveur génère le HTML (contient le contenu + les data attributes).
  2. Le navigateur affiche le HTML immédiatement (FCP rapide).
  3. Le JS React se télécharge en arrière-plan.
  4. 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éesuseState, useEffect
Markdown renderingonClick, onChange
Date formattinguseReducer, useContext
Accès DBwindow, document
Calculs lourdsAnimations JS
SEO-critical contentFormulaires 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 :

  1. Première visite → page générée et mise en cache.
  2. Requêtes suivantes → page servie depuis le cache (pendant 3600s).
  3. Après 3600s → la page est régénérée en arrière-plan (stale-while-revalidate).
  4. 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èreNode.js RuntimeEdge Runtime
EnvironnementServeur NodeV8 isolates (Cloudflare Workers, Vercel Edge)
API disponiblesToutesWeb APIs (fetch, Request, Response, crypto)
Accès filesystem
Accès DB direct❌ (via HTTP)
Démarrage à froid200-500ms< 10ms
RégionsLimitéeGlobale
Bundle sizeIllimité< 1MB (Vercel)
CoûtVariableFaible
// 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 :

  1. Streaming : envoyer le HTML dès que possible.
  2. Cache : stale-while-revalidate pour les pages non-personnalisées.
  3. Edge : déploiement global pour réduire la latence.
  4. 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

  1. Préférer Server Components sauf si interactivité nécessaire.
  2. Utiliser Suspense pour le streaming et les charges lentes.
  3. Configurer le cache à tous les niveaux (CDN, route, data).
  4. Éviter les calculs lourds dans le rendu serveur.
  5. Stratégie de revalidation adaptée (ISR > SSR pur si possible).
  6. Surveiller le TTFB comme métrique clé.
  7. Minimiser l'hydratation : moins de Client Components = meilleur TBT.
  8. 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.