Cours Next.js · Module 5: Stratégies de rendu

Stratégies de rendu dans l'App Router de Next.js

11 min de lecture
Dans cette leçon9

Next.js a révolutionné la manière de construire des applications React en proposant différentes stratégies de rendu que l'on peut choisir selon les caractéristiques de la page et les exigences du projet. Dans ce module, nous allons voir comment fonctionnent les stratégies de rendu dans le contexte de l'App Router, introduit dans Next.js 13 et développé dans les versions suivantes.

L'évolution du rendu dans Next.js

Next.js a connu une évolution importante dans son approche du rendu. Dans les versions antérieures à la 13, on utilisait des fonctions spéciales comme getStaticProps, getServerSideProps ou getStaticPaths pour déterminer la stratégie de rendu d'une page donnée. Chaque page devait choisir une stratégie précise.

Dans l'App Router, l'approche est différente : le rendu est désormais plus granulaire et repose sur le concept de React Server Components. Au lieu de fixer une stratégie pour toute la page, on peut mélanger différentes approches au niveau des composants individuels.

React Server Components - le fondement de l'App Router

Au cœur du nouveau modèle de rendu se trouvent les React Server Components (RSC), qui ont changé notre façon de penser les applications React. Les principales caractéristiques des RSC sont :

  1. Rendu côté serveur - les composants sont rendus sur le serveur, et le client reçoit un HTML prêt à l'emploi
  2. Non inclus dans le bundle JavaScript - le code des RSC n'est pas envoyé au navigateur
  3. Accès direct aux ressources serveur - bases de données, système de fichiers, etc.
  4. Pas d'accès aux API du navigateur - ils ne peuvent pas utiliser de hooks ni gérer d'événements

Stratégies de rendu dans l'App Router

Dans l'App Router, on peut toujours utiliser les stratégies de rendu traditionnelles, mais leur mise en œuvre est différente. Examinons-les une par une.

1. Server-Side Rendering (SSR)

Dans l'App Router, le SSR est le comportement par défaut des Route Handlers et des Server Components.

1// app/products/page.tsx
2import { Product } from '@/components/Product';
3
4async function getData() {
5  // Cette fonction s'exécute à chaque requête sur le serveur
6  const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
7
8  if (!res.ok) {
9    throw new Error('Failed to fetch data');
10  }
11
12  return res.json();
13}
14
15export default async function ProductsPage() {
16  const products = await getData();
17
18  return (
19    <div>
20      <h1>Nos produits</h1>
21      <div className="grid">
22        {products.map(product => (
23          <Product key={product.id} data={product} />
24        ))}
25      </div>
26    </div>
27  );
28}

Caractéristiques clés du SSR dans l'App Router :

  • On utilise l'option cache: 'no-store' dans la fonction fetch ou revalidate: 0 pour indiquer que les données doivent être récupérées à chaque requête
  • Les composants sont asynchrones (async), ce qui permet d'utiliser directement await dans le corps du composant
  • Chaque requête vers la page entraîne un nouveau rendu de la page sur le serveur

Avantages du SSR :

  • Données toujours à jour
  • Meilleure performance SEO (les moteurs de recherche voient l'intégralité du contenu de la page)
  • Possibilité de personnaliser le contenu pour chaque utilisateur

Inconvénients du SSR :

  • Chargement de page plus lent (le serveur doit traiter chaque requête)
  • Charge serveur plus élevée

2. Static Site Generation (SSG)

Dans l'App Router, le SSG est obtenu grâce au comportement par défaut de fetch, sans l'option cache: 'no-store'.

1// app/blog/page.tsx
2import { BlogPost } from '@/components/BlogPost';
3
4async function getData() {
5  // Cette fonction s'exécute uniquement au moment du build (ou à la demande en mode dev)
6  const res = await fetch('https://api.example.com/blog-posts');
7
8  if (!res.ok) {
9    throw new Error('Failed to fetch data');
10  }
11
12  return res.json();
13}
14
15export default async function BlogPage() {
16  const posts = await getData();
17
18  return (
19    <div>
20      <h1>Notre blog</h1>
21      <div className="grid">
22        {posts.map(post => (
23          <BlogPost key={post.id} data={post} />
24        ))}
25      </div>
26    </div>
27  );
28}

Caractéristiques clés du SSG dans l'App Router :

  • Par défaut, tous les composants sont rendus statiquement, sauf indication contraire
  • Les données sont récupérées au moment du build et « figées » dans le HTML statique
  • Pas besoin d'utiliser une fonction spéciale comme getStaticProps : un simple fetch suffit

Avantages du SSG :

  • Chargement de page très rapide (on sert des fichiers HTML statiques)
  • Charge serveur minimale
  • Bonne performance SEO

Inconvénients du SSG :

  • Les données peuvent être obsolètes (mises à jour uniquement lors des rebuilds)
  • Inadapté aux pages dont le contenu est personnalisé par utilisateur

3. Incremental Static Regeneration (ISR)

Dans l'App Router, l'ISR est obtenu en ajoutant l'option revalidate à la fonction fetch ou au segment de route.

1// app/products/[id]/page.tsx
2import { ProductDetails } from '@/components/ProductDetails';
3import { notFound } from 'next/navigation';
4
5// Option 1 : revalidation au niveau de la page
6export const revalidate = 3600; // Revalidation toutes les heures
7
8async function getProduct(id) {
9  // Option 2 : revalidation au niveau de la fonction fetch
10  const res = await fetch(`https://api.example.com/products/${id}`, {
11    next: { revalidate: 3600 } // Également toutes les heures
12  });
13
14  if (!res.ok) {
15    return null;
16  }
17
18  return res.json();
19}
20
21export default async function ProductPage({ params }) {
22  const product = await getProduct((await params).id);
23
24  if (!product) {
25    notFound();
26  }
27
28  return <ProductDetails product={product} />;
29}

Caractéristiques clés de l'ISR dans l'App Router :

  • On peut spécifier le temps de revalidation au niveau du composant grâce à une variable revalidate exportée
  • On peut spécifier le temps de revalidation au niveau de la requête fetch via l'option next: { revalidate: secondes }
  • Il est possible de forcer la revalidation via un API Route Handler

Avantages de l'ISR :

  • Combine les atouts du SSG (rapidité) et du SSR (fraîcheur relative des données)
  • Permet de mettre à jour le contenu sans reconstruire toute l'application
  • Bon support du SEO

Inconvénients de l'ISR :

  • Les données ne sont pas à 100 % à jour (il y a un délai dépendant du temps de revalidation défini)
  • Implémentation et débogage plus complexes

4. Client-Side Rendering (CSR)

Dans l'App Router, le CSR est obtenu en marquant un composant comme « Client Component » à l'aide de la directive 'use client' au début du fichier.

1'use client';
2
3// app/dashboard/page.tsx
4import { useState, useEffect } from 'react';
5
6export default function Dashboard() {
7  const [data, setData] = useState(null);
8  const [isLoading, setIsLoading] = useState(true);
9
10  useEffect(() => {
11    async function fetchData() {
12      try {
13        const res = await fetch('/api/dashboard-data');
14        const jsonData = await res.json();
15        setData(jsonData);
16      } catch (error) {
17        console.error('Error fetching data:', error);
18      } finally {
19        setIsLoading(false);
20      }
21    }
22
23    fetchData();
24  }, []);
25
26  if (isLoading) return <div>Chargement des données...</div>;
27
28  return (
29    <div>
30      <h1>Panneau de contrôle</h1>
31      {/* Affichage des données récupérées côté client */}
32      {data && data.map(item => (
33        <div key={item.id}>{item.name}</div>
34      ))}
35    </div>
36  );
37}

Caractéristiques clés du CSR dans l'App Router :

  • Le composant doit être marqué avec la directive 'use client'
  • On peut utiliser les hooks React (useState, useEffect, etc.)
  • Les données sont récupérées après le chargement de la page dans le navigateur

Avantages du CSR :

  • Peut offrir une meilleure expérience utilisateur pour les applications interactives
  • Charge serveur plus faible (les données sont récupérées directement depuis l'API dans le navigateur)
  • Bonne option pour du contenu personnalisé pour un utilisateur connecté

Inconvénients du CSR :

  • Performance SEO plus faible (les moteurs de recherche peuvent ne pas voir tout le contenu)
  • Chargement initial du contenu plus lent (l'utilisateur voit un état de chargement)
  • Taille du fichier JavaScript envoyé au navigateur plus importante

Approche mixte - la bonne pratique de l'App Router

L'un des plus grands avantages de l'App Router est la possibilité de mélanger différentes stratégies de rendu au sein d'une même application, et même d'une même page. Cela nous permet de choisir la stratégie optimale pour chaque composant.

Exemple d'utilisation de stratégies mixtes :

1// app/products/[category]/page.tsx
2import { ProductList } from '@/components/ProductList';
3import { CategoryHeader } from '@/components/CategoryHeader';
4import FilterSidebar from '@/components/FilterSidebar'; // Client Component
5
6// Cette page et ses données sont rendues statiquement
7// avec une revalidation toutes les heures
8export const revalidate = 3600;
9
10// On précise quelles catégories doivent être générées statiquement
11export async function generateStaticParams() {
12  const categories = await fetch('https://api.example.com/categories').then(res => res.json());
13
14  return categories.map(category => ({
15    category: category.slug,
16  }));
17}
18
19async function getCategoryData(category) {
20  const categoryData = await fetch(`https://api.example.com/categories/${category}`);
21  return categoryData.json();
22}
23
24async function getProducts(category) {
25  const products = await fetch(`https://api.example.com/products?category=${category}`);
26  return products.json();
27}
28
29export default async function CategoryPage({ params }) {
30  // Récupération des données en parallèle
31  const [categoryData, products] = await Promise.all([
32    getCategoryData((await params).category),
33    getProducts((await params).category),
34  ]);
35
36  return (
37    <div className="grid grid-cols-4 gap-4">
38      {/* En-tête de catégorie rendu statiquement */}
39      <CategoryHeader data={categoryData} />
40
41      {/* Filtre interactif rendu côté client */}
42      <div className="col-span-1">
43        <FilterSidebar initialProducts={products} />
44      </div>
45
46      {/* Liste de produits rendue statiquement */}
47      <div className="col-span-3">
48        <ProductList products={products} />
49      </div>
50    </div>
51  );
52}

Dans cet exemple :

  • Toute la page est rendue statiquement avec une revalidation horaire (ISR)
  • Seules certaines catégories sont générées au moment du build (generateStaticParams)
  • Le composant FilterSidebar est un Client Component, permettant un filtrage interactif côté client
  • Les composants CategoryHeader et ProductList sont des Server Components rendus statiquement

Quand utiliser quelle stratégie ?

Le choix de la stratégie de rendu dépend des caractéristiques de la partie concernée de l'application :

  1. Pages statiques (documentation, landing pages, pages d'information) :

    • Utilise le SSG (comportement par défaut)
  2. Contenu dynamique ne nécessitant pas les données les plus récentes (blog, catalogue produits) :

    • Utilise l'ISR avec un temps de revalidation approprié
  3. Contenu nécessitant les données les plus récentes (cours de bourse, actualités, résultats sportifs) :

    • Utilise le SSR (cache: 'no-store')
  4. Contenu personnalisé et éléments d'UI interactifs (tableaux de bord, formulaires) :

    • Utilise le CSR ('use client')
  5. Approche hybride (par ex. e-commerce) :

    • Squelette de page statique (SSG/ISR)
    • Données produits dynamiques (SSR)
    • Éléments interactifs comme le panier, les filtres (CSR)

Routes dynamiques et statiques

Dans l'App Router, on peut aussi préciser quels chemins dynamiques doivent être générés statiquement lors du build de l'application :

1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
4
5  return posts.map((post) => ({
6    slug: post.slug,
7  }));
8}

Pour les routes non couvertes par generateStaticParams :

  • Si on définit dynamicParams = false dans les options du segment, Next.js renverra une erreur 404
  • Si dynamicParams = true (par défaut) ou n'est pas défini, Next.js générera la page à la demande (SSR)

Optimiser les pages dynamiques

Si on a beaucoup de sous-pages dynamiques (par ex. des milliers de produits en e-commerce), on peut optimiser le processus de build en :

  1. Générant statiquement les pages les plus populaires pendant le build :
1export async function generateStaticParams() {
2  // Récupère uniquement les produits les plus populaires
3  const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
4
5  return popularProducts.map((product) => ({
6    id: product.id,
7  }));
8}
  1. On-demand Revalidation - revalidation de pages individuelles à la demande, par ex. après la mise à jour d'un produit :
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6  const { path, secret } = await request.json();
7
8  // Vérification de la clé secrète pour la sécurité
9  if (secret !== process.env.REVALIDATION_SECRET) {
10    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
11  }
12
13  if (!path) {
14    return NextResponse.json({ message: 'Path is required' }, { status: 400 });
15  }
16
17  try {
18    // Revalidation du chemin spécifié
19    revalidatePath(path);
20    return NextResponse.json({ revalidated: true, message: `Revalidated ${path}` });
21  } catch (err) {
22    return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
23  }
24}

Comparaison des stratégies de rendu

CaractéristiqueSSGISRSSRCSR
Performance
SEO
Fraîcheur des données
Charge serveur
Interactivité

Résumé

L'App Router de Next.js offre des capacités avancées pour mélanger différentes stratégies de rendu au niveau des composants, ce qui permet de créer des applications performantes et flexibles. La clé du succès est de choisir la bonne stratégie pour chaque partie de l'application, en tenant compte des exigences de performance, de SEO et de fraîcheur des données.

Dans le module suivant, nous détaillerons les mécanismes de revalidation des données dans l'App Router de Next.js, essentiels pour mettre en œuvre l'ISR et garantir la fraîcheur du contenu.

Code de cette leçon : App.tsx
1// Démo : Stratégies de rendu dans Next.js App Router
2// SSG (Static), SSR (Dynamic), ISR (Incremental Static Regeneration)
3import React, { useState } from 'react';
4
5// Next.js App Router uses Static Rendering (SSG) by default
6// Dynamic rendering is enabled automatically when:
7// - You use cookies(), headers(), searchParams
8// - fetch() avec { cache: 'no-store' }
9// - export const dynamic = 'force-dynamic'
10
11interface RenderingStrategy {
12  name: string;
13  shortName: string;
14  color: string;
15  when: string;
16  code: string;
17  pros: string[];
18  cons: string[];
19  buildTime: boolean;
20  requestTime: boolean;
21}
22
23const strategies: RenderingStrategy[] = [
24  {
25    name: 'Static Site Generation',
26    shortName: 'SSG',
27    color: '#7c4dff',
28    when: 'Build time (default in App Router)',
29    code: "// Default behavior - no additional code needed\nexport default async function Page() {\n  const data = await fetch(url); // cached\n  return <div>{data}</div>;\n}",
30    pros: ['Fastest - HTML ready from CDN', 'Cheapest - zero computations per request', 'SEO-friendly'],
31    cons: ['Static data until next build', 'Not suitable for personalization'],
32    buildTime: true,
33    requestTime: false,
34  },
35  {
36    name: 'Server-Side Rendering',
37    shortName: 'SSR',
38    color: '#64ffda',
39    when: 'Every request (on-demand)',
40    code: "// Force dynamic rendering\nexport const dynamic = 'force-dynamic';\n// or use cookies/headers\nimport { cookies } from 'next/headers';\n\nexport default async function Page() {\n  const session = cookies().get('token');\n  const data = await fetch(url, {\n    cache: 'no-store'\n  });\n  return <div>{data}</div>;\n}",
41    pros: ['Always up-to-date data', 'Personnalisation par utilisateur', 'SEO-friendly'],
42    cons: ['Slower - server processes every request', 'Higher server costs'],
43    buildTime: false,
44    requestTime: true,
45  },
46  {
47    name: 'Incremental Static Regeneration',
48    shortName: 'ISR',
49    color: '#ff9800',
50    when: 'Build + refresh every N seconds',
51    code: "// Revalidation toutes les 60 secondes\nexport const revalidate = 60;\n\nexport default async function Page() {\n  const data = await fetch(url, {\n    next: { revalidate: 60 }\n  });\n  return <div>{data}</div>;\n}\n\n// Ou on-demand :\nimport { revalidatePath } from 'next/cache';\nrevalidatePath('/products');",
52    pros: ['Balance: SSG speed + SSR freshness', 'Low costs', 'Scalable'],
53    cons: ['Data can be stale for N seconds', 'More complex configuration'],
54    buildTime: true,
55    requestTime: true,
56  },
57];
58
59function StrategyCard({ strategy, isActive, onClick }: {
60  strategy: RenderingStrategy; isActive: boolean; onClick: () => void;
61}) {
62  return (
63    <div onClick={onClick} style={{
64      background: isActive ? `${strategy.color}12` : 'rgba(255,255,255,0.02)',
65      border: `2px solid ${isActive ? strategy.color : 'rgba(255,255,255,0.08)'}`,
66      borderRadius: '12px', padding: '20px', cursor: 'pointer',
67      transition: 'all 0.3s ease',
68    }}>
69      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
70        <h3 style={{ color: strategy.color, margin: 0 }}>{strategy.shortName}</h3>
71        <div style={{ display: 'flex', gap: '6px' }}>
72          {strategy.buildTime && (
73            <span style={{ background: '#7c4dff20', color: '#7c4dff', padding: '2px 8px', borderRadius: '10px', fontSize: '0.7rem' }}>
74              Build
75            </span>
76          )}
77          {strategy.requestTime && (
78            <span style={{ background: '#64ffda20', color: '#64ffda', padding: '2px 8px', borderRadius: '10px', fontSize: '0.7rem' }}>
79              Request
80            </span>
81          )}
82        </div>
83      </div>
84      <p style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '0 0 8px' }}>{strategy.name}</p>
85      <p style={{ color: '#78909c', fontSize: '0.8rem', margin: 0 }}>{strategy.when}</p>
86    </div>
87  );
88}
89
90export default function RenderingStrategies() {
91  const [active, setActive] = useState(0);
92  const s = strategies[active];
93
94  return (
95    <div style={{
96      background: '#0f0f23', minHeight: '100vh', padding: '24px',
97      color: '#fff', fontFamily: 'system-ui, sans-serif',
98    }}>
99      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Rendering Strategies</h1>
100      <p style={{ color: '#b0bec5', marginBottom: '24px' }}>
101        Next.js App Router - SSG vs SSR vs ISR
102      </p>
103
104      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', marginBottom: '24px' }}>
105        {strategies.map((st, i) => (
106          <StrategyCard key={st.shortName} strategy={st} isActive={i === active} onClick={() => setActive(i)} />
107        ))}
108      </div>
109
110      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
111        <div style={{
112          background: 'rgba(0,0,0,0.3)', borderRadius: '12px', padding: '20px',
113        }}>
114          <h4 style={{ color: s.color, marginBottom: '12px' }}>Code</h4>
115          <pre style={{ margin: 0, color: '#b0bec5', fontSize: '0.8rem', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
116            {s.code}
117          </pre>
118        </div>
119        <div style={{ display: 'grid', gap: '16px' }}>
120          <div style={{ background: 'rgba(76,175,80,0.08)', borderRadius: '12px', padding: '16px' }}>
121            <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Avantages</h4>
122            {s.pros.map(p => (
123              <p key={p} style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '4px 0' }}>+ {p}</p>
124            ))}
125          </div>
126          <div style={{ background: 'rgba(244,67,54,0.08)', borderRadius: '12px', padding: '16px' }}>
127            <h4 style={{ color: '#f44336', marginBottom: '8px' }}>Inconvénients</h4>
128            {s.cons.map(c => (
129              <p key={c} style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '4px 0' }}>- {c}</p>
130            ))}
131          </div>
132        </div>
133      </div>
134    </div>
135  );
136}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que signifie le rendu côté serveur (SSR) dans Next.js ?

Articles utiles