Cours Next.js · Module 3: Styles et optimisation visuelle
Benchmarks de performance dans Next.js 16 - SSR vs SSG vs ISR
Dans cette leçon7
À Quantum Metropolis, les ingénieurs surveillent et optimisent en permanence la performance de tous les systèmes de la ville - des réseaux d'énergie aux réseaux de transport. Chaque décision d'architecture influe sur la vitesse et l'efficacité de l'ensemble de la ville. De même, dans Next.js 16, le choix de la bonne stratégie de rendu a un impact fondamental sur la performance de l'application.
Dans ce chapitre, tu découvriras une comparaison détaillée des stratégies de rendu, des métriques de performance et des exemples pratiques qui t'aideront à prendre des décisions d'architecture éclairées.
Trois grandes stratégies de rendu dans Next.js 16
1. SSR - Server-Side Rendering (rendu côté serveur)
Comment ça fonctionne : Chaque requête utilisateur déclenche le rendu de la page sur le serveur en temps réel.
1// app/dashboard/page.tsx
2// Since Next.js 15, fetch is not cached by default, but a page without request-time APIs
3// would still be rendered at build time, so we explicitly force rendering on every request
4export const dynamic = 'force-dynamic';
5// LUB
6// fetch(..., { cache: 'no-store' })
7
8async function getDashboardData() {
9 const res = await fetch('https://api.quantum.com/dashboard', {
10 cache: 'no-store' // Wymusza SSR
11 });
12 return res.json();
13}
14
15export default async function Dashboard() {
16 const data = await getDashboardData();
17
18 return (
19 <div>
20 <h1>Panel kontrolny</h1>
21 <p>Ostatnia aktualizacja: {new Date().toLocaleTimeString()}</p>
22 <DashboardContent data={data} />
23 </div>
24 );
25}Avantages :
- Des données toujours fraîches
- Excellent pour le contenu personnalisé
- Du contenu dynamique sans délai
Inconvénients :
- Un TTFB (Time To First Byte) plus lent
- Une charge serveur plus élevée
- Peut être plus coûteux à héberger
2. SSG - Static Site Generation (génération statique)
Comment ça fonctionne : Les pages sont générées une seule fois au moment du build et servies sous forme de fichiers HTML statiques.
1// app/blog/[slug]/page.tsx
2
3async function getPost(slug: string) {
4 const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
5 cache: 'force-cache' // Wymusza SSG
6 });
7 return res.json();
8}
9
10export async function generateStaticParams() {
11 const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());
12
13 return posts.map((post) => ({
14 slug: post.slug,
15 }));
16}
17
18export default async function BlogPost({
19 params,
20}: {
21 params: Promise<{ slug: string }>;
22}) {
23 const post = await getPost((await params).slug);
24
25 return (
26 <article>
27 <h1>{post.title}</h1>
28 <time>{post.publishedAt}</time>
29 <div dangerouslySetInnerHTML={{ __html: post.content }} />
30 </article>
31 );
32}Avantages :
- Un TTFB très rapide
- De faibles coûts d'hébergement (CDN)
- Excellent pour le SEO
- La meilleure performance
Inconvénients :
- Les données peuvent être obsolètes
- Un temps de build long pour les grands sites
- Nécessite un rebuild à chaque modification
3. ISR - Incremental Static Regeneration
Comment ça fonctionne : Les pages sont générées statiquement mais peuvent être rafraîchies en arrière-plan après un délai défini.
1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4 const res = await fetch(`https://api.quantum.com/products/${id}`, {
5 next: { revalidate: 3600 } // Revalidation every hour
6 });
7 return res.json();
8}
9
10export async function generateStaticParams() {
11 // Générer uniquement les produits les plus populaires lors du build
12 const popularProducts = await fetch('https://api.quantum.com/products/popular')
13 .then(res => res.json());
14
15 return popularProducts.map((product) => ({
16 id: product.id.toString(),
17 }));
18}
19
20export default async function ProductPage({
21 params,
22}: {
23 params: Promise<{ id: string }>;
24}) {
25 const product = await getProduct((await params).id);
26
27 return (
28 <div>
29 <h1>{product.name}</h1>
30 <p>Cena: {product.price} QC</p>
31 <p>Availability: {product.stock} units</p>
32 <button>Ajouter au panier</button>
33 </div>
34 );
35}
36
37export const revalidate = 3600; // Globalny czas rewalidacjiAvantages :
- Un compromis entre SSR et SSG
- Un TTFB rapide comme le SSG
- Du contenu rafraîchi automatiquement
- Des pages générées à la demande
Inconvénients :
- Les premiers utilisateurs peuvent voir des données périmées
- Nécessite de configurer le temps de revalidation
- Une logique de cache plus complexe
Comparaison des métriques de performance
TTFB - Time To First Byte
Le TTFB est le temps entre l'envoi d'une requête et la réception du premier octet de la réponse. À Metropolis Quantum, c'est comme le temps de réaction du système à une commande : plus c'est rapide, mieux c'est.
Benchmark (valeurs d'exemple) :
| Stratégie | TTFB | Note |
|---|---|---|
| SSG | 50-100ms | Excellent |
| ISR (en cache) | 50-150ms | Très bon |
| ISR (en revalidation) | 200-500ms | Bon |
| SSR | 300-800ms | Moyen |
| SSR (API lente) | 1000ms+ | Faible |
1// Measuring TTFB in the browser
2performance.getEntriesByType('navigation')[0].responseStart;LCP - Largest Contentful Paint
Le LCP mesure le temps avant l'affichage du plus grand élément de contenu. C'est comme la vitesse à laquelle l'hologramme principal apparaît à Metropolis Quantum.
Benchmark :
| Stratégie | LCP | Note |
|---|---|---|
| SSG | 0.5-1.5s | Excellent |
| ISR | 0.8-2s | Très bon |
| SSR | 1.5-3s | Bon |
| SSR (lourd) | 3s+ | Nécessite une optimisation |
Recommandations de Google :
- Bon : < 2.5s
- À améliorer : 2.5s - 4s
- Faible : > 4s
1// Monitoring LCP
2import { onLCP } from 'web-vitals';
3
4onLCP((metric) => {
5 console.log('LCP:', metric.value);
6 // Send to analytics
7 analytics.track('Web Vitals', {
8 metric: 'LCP',
9 value: metric.value,
10 rating: metric.rating
11 });
12});CLS - Cumulative Layout Shift
Le CLS mesure la stabilité visuelle d'une page. C'est comme la stabilité des interfaces holographiques à Metropolis Quantum : elles ne doivent pas « sauter ».
Benchmark :
| Stratégie | CLS | Note |
|---|---|---|
| SSG | 0-0.05 | Excellent |
| ISR | 0-0.1 | Très bon |
| SSR | 0.1-0.25 | Bon (s'il est bien optimisé) |
Recommandations de Google :
- Bon : < 0.1
- À améliorer : 0.1 - 0.25
- Faible : > 0.25
1// Monitoring CLS
2import { onCLS } from 'web-vitals';
3
4onCLS((metric) => {
5 console.log('CLS:', metric.value);
6 analytics.track('Web Vitals', {
7 metric: 'CLS',
8 value: metric.value,
9 rating: metric.rating
10 });
11});Quand utiliser quelle stratégie ?
Choisis le SSG quand :
Blog ou page marketing
- Le contenu change rarement
- Le SEO est une priorité
- Tu as besoin de la meilleure performance
1// app/blog/[slug]/page.tsx - Idéal pour le SSG
2export async function generateStaticParams() {
3 const posts = await getPosts();
4 return posts.map(post => ({ slug: post.slug }));
5}
6
7export default async function BlogPost({ params }) {
8 const post = await getPost((await params).slug);
9 return <Article post={post} />;
10}Page de documentation
- Du contenu statique ou rarement mis à jour
- Une prise en charge multilingue
- La vitesse de chargement est critique
Landing pages
- Les campagnes marketing
- Les produits/services
- Forte conversion = chargement rapide
Choisis l'ISR quand :
E-commerce (pages produit)
- Les données changent régulièrement (prix, stock)
- Un grand nombre de produits
- Un équilibre entre fraîcheur et performance
1// app/products/[id]/page.tsx - Idéal pour l'ISR
2export const revalidate = 300; // 5 minut
3
4export default async function Product({ params }) {
5 const product = await getProduct((await params).id);
6
7 return (
8 <div>
9 <h1>{product.name}</h1>
10 <p>Cena: {product.price}</p>
11 <p>W magazynie: {product.stock}</p>
12 </div>
13 );
14}Newsroom / Médias
- Des articles mis à jour plusieurs fois par jour
- Beaucoup de contenu
- Un besoin de vitesse + de fraîcheur
Profils utilisateur (publics)
- Les données changent rarement
- Un grand nombre de profils
- Un besoin de cache avec rafraîchissement
Choisis le SSR quand :
Tableau de bord / Panneau d'administration
- Des données en temps réel
- La personnalisation
- L'autorisation des utilisateurs
1// app/dashboard/page.tsx - Idéal pour le SSR
2export const dynamic = 'force-dynamic';
3
4export default async function Dashboard() {
5 const session = await getServerSession();
6 const userData = await getUserData(session.userId);
7
8 return (
9 <div>
10 <h1>Bienvenue, {userData.name}</h1>
11 <RealTimeAnalytics userId={session.userId} />
12 </div>
13 );
14}Résultats de recherche
- Des requêtes dynamiques
- Des résultats personnalisés
- Des données toujours fraîches
Contenu propre à l'utilisateur
- Le panier d'achat
- Les préférences de l'utilisateur
- L'historique d'activité
Exemples concrets avec benchmarks
Exemple 1 : blog tech (SSG)
1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5 const post = await getPost((await params).slug);
6
7 return {
8 title: post.title,
9 description: post.excerpt,
10 openGraph: {
11 images: [post.coverImage],
12 },
13 };
14}
15
16export async function generateStaticParams() {
17 const posts = await getAllPosts();
18
19 return posts.map((post) => ({
20 slug: post.slug,
21 }));
22}
23
24async function getPost(slug: string) {
25 const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
26 cache: 'force-cache'
27 });
28 return res.json();
29}
30
31export default async function BlogPost({ params }) {
32 const post = await getPost((await params).slug);
33
34 return (
35 <article>
36 <h1>{post.title}</h1>
37 <time>{post.publishedAt}</time>
38 <Image
39 src={post.coverImage}
40 alt={post.title}
41 width={1200}
42 height={630}
43 preload
44 />
45 <div dangerouslySetInnerHTML={{ __html: post.content }} />
46 </article>
47 );
48}Métriques :
- TTFB : ~80ms
- LCP : ~1.2s
- CLS : 0.02
- Temps de build : 2min pour 500 articles
- Note :
Exemple 2 : marketplace e-commerce (ISR)
1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4 const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
5 next: { revalidate: 600 } // Rewalidacja co 10 minut
6 });
7
8 if (!res.ok) {
9 notFound();
10 }
11
12 return res.json();
13}
14
15export async function generateStaticParams() {
16 // Generate only top 100 products during build
17 const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
18 .then(res => res.json());
19
20 return topProducts.map((product) => ({
21 id: product.id.toString(),
22 }));
23}
24
25export default async function ProductPage({ params }) {
26 const product = await getProduct((await params).id);
27
28 return (
29 <div className="product-page">
30 <ProductGallery images={product.images} />
31
32 <div className="product-info">
33 <h1>{product.name}</h1>
34
35 <div className="price-stock">
36 <span className="price">{product.price} QC</span>
37 <StockIndicator stock={product.stock} />
38 </div>
39
40 <div className="description">
41 {product.description}
42 </div>
43
44 <AddToCartButton productId={product.id} />
45
46 <ProductReviews productId={product.id} />
47 </div>
48
49 <RelatedProducts categoryId={product.categoryId} />
50 </div>
51 );
52}
53
54export const revalidate = 600; // 10 minutMétriques :
- TTFB : ~120ms (en cache), ~400ms (en revalidation)
- LCP : ~1.8s
- CLS : 0.05
- Temps de build : 5min pour les 100 produits principaux
- Génération à la demande : ~500ms pour le reste
- Note :
Exemple 3 : tableau de bord temps réel (SSR)
1// app/dashboard/analytics/page.tsx
2
3export const dynamic = 'force-dynamic';
4export const fetchCache = 'force-no-store';
5
6async function getRealTimeAnalytics(userId: string) {
7 const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
8 cache: 'no-store',
9 headers: {
10 Authorization: `Bearer ${process.env.API_TOKEN}`
11 }
12 });
13
14 return res.json();
15}
16
17export default async function AnalyticsDashboard() {
18 const session = await getServerSession();
19
20 if (!session) {
21 redirect('/login');
22 }
23
24 const analytics = await getRealTimeAnalytics(session.user.id);
25
26 return (
27 <div className="analytics-dashboard">
28 <h1>Analityka w czasie rzeczywistym</h1>
29
30 <div className="stats-grid">
31 <StatCard
32 title="Active Users"
33 value={analytics.activeUsers}
34 change={analytics.activeUsersChange}
35 />
36
37 <StatCard
38 title="Przychody dzisiaj"
39 value={`${analytics.todayRevenue} QC`}
40 change={analytics.revenueChange}
41 />
42
43 <StatCard
44 title="Konwersje"
45 value={`${analytics.conversionRate}%`}
46 change={analytics.conversionChange}
47 />
48 </div>
49
50 <RevenueChart data={analytics.revenueChart} />
51 <UserActivityFeed data={analytics.recentActivity} />
52 </div>
53 );
54}Métriques :
- TTFB : ~450ms
- LCP : ~2.4s
- CLS : 0.08
- Fraîcheur des données : toujours à jour
- Note : (bon pour le temps réel, mais nécessite une optimisation)
Exemple 4 : approche hybride - fil d'actualités (ISR + mises à jour côté client)
1// app/news/page.tsx
2
3async function getLatestNews() {
4 const res = await fetch('https://api.quantum-news.com/latest', {
5 next: { revalidate: 60 } // Revalidation every minute
6 });
7
8 return res.json();
9}
10
11export default async function NewsFeed() {
12 const initialNews = await getLatestNews();
13
14 return (
15 <div className="news-feed">
16 <h1>News from Metropolis Quantum</h1>
17
18 {/* Server-rendered initial content (ISR) */}
19 <NewsList initialNews={initialNews} />
20
21 {/* Client-side updates co 30 sekund */}
22 <ClientNewsUpdater />
23 </div>
24 );
25}
26
27export const revalidate = 60;1// components/ClientNewsUpdater.tsx
2'use client';
3
4import { useEffect, useState } from 'react';
5
6export function ClientNewsUpdater() {
7 const [latestNews, setLatestNews] = useState([]);
8
9 useEffect(() => {
10 const interval = setInterval(async () => {
11 const res = await fetch('/api/news/latest');
12 const data = await res.json();
13 setLatestNews(data);
14 }, 30000); // 30 sekund
15
16 return () => clearInterval(interval);
17 }, []);
18
19 if (latestNews.length === 0) return null;
20
21 return (
22 <div className="breaking-news">
23 <h2>Latest News</h2>
24 {latestNews.map(news => (
25 <NewsCard key={news.id} news={news} />
26 ))}
27 </div>
28 );
29}Métriques :
- TTFB initial : ~150ms
- LCP : ~1.5s
- CLS : 0.04
- Fraîcheur : ISR toutes les minutes + mises à jour côté client toutes les 30s
- Note : (le meilleur des deux mondes)
Outils de surveillance de la performance
1. Web Vitals dans Next.js 16
1// app/layout.tsx
2import { SpeedInsights } from '@vercel/speed-insights/next';
3import { Analytics } from '@vercel/analytics/react';
4
5export default function RootLayout({ children }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <SpeedInsights />
11 <Analytics />
12 </body>
13 </html>
14 );
15}2. Reporting personnalisé des Web Vitals
1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7 useReportWebVitals((metric) => {
8 // Sending to your own analytics
9 const body = JSON.stringify({
10 name: metric.name,
11 value: metric.value,
12 rating: metric.rating,
13 delta: metric.delta,
14 id: metric.id,
15 });
16
17 fetch('/api/analytics/web-vitals', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/json' },
20 body,
21 });
22 });
23
24 return null;
25}3. Lighthouse CI dans le pipeline
1# .github/workflows/lighthouse.yml
2name: Lighthouse CI
3
4on:
5 pull_request:
6 branches: [main]
7
8jobs:
9 lighthouse:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - uses: actions/setup-node@v3
14
15 - name: Install dependencies
16 run: npm ci
17
18 - name: Build
19 run: npm run build
20
21 - name: Run Lighthouse CI
22 run: |
23 npm install -g @lhci/cli
24 lhci autorun
25
26 - name: Upload results
27 uses: actions/upload-artifact@v3
28 with:
29 name: lighthouse-results
30 path: .lighthouseciOptimisation pour chaque stratégie
Optimisations SSG
1// next.config.js
2module.exports = {
3 images: {
4 formats: ['image/avif', 'image/webp'],
5 deviceSizes: [640, 750, 828, 1080, 1200],
6 },
7 compress: true,
8 poweredByHeader: false,
9 generateEtags: true,
10};Optimisations ISR
1// Use on-demand revalidation for important updates
2// app/api/revalidate/route.ts
3import { revalidatePath } from 'next/cache';
4import { NextRequest } from 'next/server';
5
6export async function POST(request: NextRequest) {
7 const secret = request.nextUrl.searchParams.get('secret');
8
9 if (secret !== process.env.REVALIDATION_TOKEN) {
10 return Response.json({ message: 'Invalid token' }, { status: 401 });
11 }
12
13 const path = request.nextUrl.searchParams.get('path');
14
15 if (path) {
16 revalidatePath(path);
17 return Response.json({ revalidated: true });
18 }
19
20 return Response.json({ message: 'Missing path' }, { status: 400 });
21}Optimisations SSR
1// Use streaming for faster TTFB
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4
5export default function Dashboard() {
6 return (
7 <div>
8 <h1>Dashboard</h1>
9
10 <Suspense fallback={<AnalyticsSkeleton />}>
11 <Analytics />
12 </Suspense>
13
14 <Suspense fallback={<ChartSkeleton />}>
15 <RevenueChart />
16 </Suspense>
17 </div>
18 );
19}Résumé et arbre de décision
1Les données doivent-elles être en temps réel ?
2├─ OUI → SSR
3│ └─ Envisage le streaming et Suspense pour de meilleures performances
4│
5└─ NON
6 │
7 ├─ Les données changent-elles plus d'une fois par heure ?
8 │ ├─ OUI → ISR (revalidate : 60-3600)
9 │ │ └─ Envisage la revalidation à la demande pour les mises à jour importantes
10 │ │
11 │ └─ NON → SSG
12 │ └─ Utilise generateStaticParams pour tous les chemins possibles
13 │
14 └─ As-tu besoin de personnalisation ?
15 ├─ OUI → SSR ou ISR + Client Components
16 └─ NON → SSG ou ISRConclusions clés :
- SSG - le plus rapide, mais statique (blogs, marketing, documentation)
- ISR - l'équilibre (e-commerce, actualités, profils publics)
- SSR - toujours frais (tableaux de bord, recherche, contenu utilisateur)
- Hybride - souvent la meilleure solution (ISR + mises à jour côté client)
Tout comme les ingénieurs de Quantum Metropolis optimisent chaque système pour une performance maximale, tu devrais toi aussi choisir consciemment la stratégie de rendu pour chaque partie de ton application. Il n'existe pas de « meilleure » solution unique - la clé est d'adapter la stratégie au cas d'usage précis.
Surveille les Web Vitals, teste différentes approches, et place toujours l'expérience utilisateur en premier. À l'ère des technologies quantiques, chaque milliseconde compte !
Code de cette leçon : App.tsx
1// CSS Modules - Création de styles scoped dans Next.js
2import React from 'react';
3import styles from './Card.module.css';
4
5// Ce composant démontre les CSS Modules dans Next.js
6// Les classes CSS sont automatiquement uniques (scoped)
7
8export default function CSSModulesDemo() {
9 return (
10 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff' }}>
11 <h1 style={{ color: '#64ffda', marginBottom: '20px' }}>CSS Modules dans Next.js</h1>
12
13 <div className={styles.card}>
14 <h2 className={styles.cardTitle}>Quantum Energy Monitor</h2>
15 <p className={styles.cardDescription}>
16 Surveillance du niveau énergétique dans le secteur Alpha
17 </p>
18 <span className={styles.badge}>Active</span>
19 </div>
20
21 <div className={styles.card}>
22 <h2 className={styles.cardTitle}>Transport Hub</h2>
23 <p className={styles.cardDescription}>
24 État du réseau de transport de Metropolis
25 </p>
26 <span className={styles.badge}>Online</span>
27 </div>
28 </div>
29 );
30}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. Quelle option de fetch force la génération statique (SSG) dans Next.js 16 selon la leçon ?
2. Comment la leçon a-t-elle configuré l’ISR (Incremental Static Regeneration) ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Construis un design system complet pour une application Next.js : 1. Configuration - crée une structure de dossiers pour le design system 2. Design tokens - définis les couleurs, la typographie et les espacements à l’aide de CSS custom properties 3. Layout system - crée un système de grille avec CSS Grid et Flexbox 4. Component library - construis les composants Button, Card et Modal avec leurs variantes 5. Theme system - implémente le mode clair/sombre avec des variables CSS 6. Responsive system - crée des breakpoints et des classes utilitaires 7. Animation system - ajoute des micro-interactions et des transitions 8. Accessibility - implémente les états de focus et les attributs ARIA Utilise toutes les techniques que tu as apprises : CSS Modules pour les composants, Tailwind pour les utilitaires, CSS-in-JS pour les styles dynamiques.