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

Edge Runtime et optimisation des performances

22 min de lecture
Dans cette leçon8

Dans le monde des applications web modernes, la performance est un facteur clé qui influence l'expérience utilisateur et la réussite d'un projet. Next.js propose des outils avancés d'optimisation des performances, parmi lesquels l'Edge Runtime se distingue comme une solution particulièrement puissante. Dans ce module, nous allons voir l'Edge Runtime ainsi que d'autres stratégies d'optimisation des performances dans les applications Next.js.

L'Edge Runtime dans Next.js

Qu'est-ce que l'Edge Runtime ?

L'Edge Runtime est un environnement d'exécution léger qui permet d'exécuter du code au plus près de l'utilisateur final, en « bordure » du réseau, plutôt que dans un centre de données central. Next.js permet d'exécuter des Route Handlers et des pages dans l'Edge Runtime, ce qui apporte :

  1. Une latence plus faible - le code s'exécute plus près de l'utilisateur
  2. Un temps jusqu'au premier octet (TTFB) plus court - l'envoi de la réponse commence plus tôt
  3. Une mise à l'échelle mondiale - distribution automatique dans le monde entier
  4. Une consommation de mémoire réduite - grâce à un runtime plus léger

Différences entre l'Edge Runtime et le Runtime Node.js

Il est utile de bien comprendre les différences entre l'Edge Runtime et l'environnement Node.js traditionnel :

CaractéristiqueEdge RuntimeRuntime Node.js
Temps de démarrage à froidTrès rapide (millisecondes)Plus lent (centaines de millisecondes)
ÉvolutivitéAutomatique, mondialeNécessite une configuration
Accès aux APIEnsemble limitéAPI Node.js complète
Taille du codeLimitée (généralement 1-4 Mo)Pratiquement illimitée
Accès au système de fichiersAucunAccès complet
Durée d'exécution d'une fonctionLimitée (par ex. 50-60 ms)Plus longue (minutes)
Cas d'usage typiquesAPI, pages au contenu dynamiqueOpérations complexes, accès aux bases de données

Configurer l'Edge Runtime dans Next.js

Pour exécuter un composant de page ou un Route Handler dans l'Edge Runtime, on utilise l'export runtime dans le fichier :

1// app/api/edge-route/route.ts
2import { NextRequest } from 'next/server';
3
4export const runtime = 'edge'; // Déclaration de l'Edge Runtime
5
6export async function GET(request: NextRequest) {
7  const { searchParams } = new URL(request.url);
8  const query = searchParams.get('query') || 'Monde';
9
10  // Traitement de la requête avec une latence minimale
11  return Response.json({ message: `Bonjour, ${query} !` });
12}

Pour une page ou un composant de Layout :

1// app/edge-page/page.tsx
2import { cookies } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function EdgePage() {
7  const cookie = (await cookies()).get('theme')?.value || 'light';
8
9  return (
10    <div>
11      <h1>Page rendue dans l'Edge Runtime</h1>
12      <p>Ton thème : {cookie}</p>
13    </div>
14  );
15}

Quand utiliser l'Edge Runtime ?

L'Edge Runtime est idéal pour :

  1. La personnalisation dynamique - par ex. la géolocalisation, les préférences de l'utilisateur
  2. Les proxys d'API - transmettre des requêtes à d'autres services avec une latence minimale
  3. Les tests A/B - décider rapidement quelle version afficher
  4. Les pages dynamiques simples - qui ne nécessitent pas d'opérations complexes
  5. Un public international - quand la latence du réseau compte

Exemple de géolocalisation dans l'Edge Runtime :

1// app/edge-geo/page.tsx
2import { headers } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function GeoPage() {
7  const headersList = await headers();
8  const countryCode = headersList.get('x-vercel-ip-country') || 'Unknown';
9  const city = headersList.get('x-vercel-ip-city') || 'Unknown City';
10
11  return (
12    <div>
13      <h1>Bienvenue, visiteur !</h1>
14      <p>Nous avons détecté que tu viens de : {city}, {countryCode}</p>
15      {countryCode === 'PL' && (
16        <p>Contenu spécial pour les visiteurs venant de Pologne !</p>
17      )}
18    </div>
19  );
20}

Optimisation des images et des médias

Next.js propose des solutions intégrées d'optimisation des médias qui ont un impact considérable sur les performances de l'application.

Le composant Image de Next.js

Le composant Image de Next.js optimise automatiquement les images, ce qui réduit la quantité de données transférées et améliore les métriques Web Vitals :

1import Image from 'next/image';
2
3export default function OptimizedImages() {
4  return (
5    <div>
6      <h2>Images optimisées dans Next.js</h2>
7
8      <Image
9        src="/hero-image.jpg" // Chemin relatif dans le dossier public
10        alt="Hero Image"
11        width={1200}
12        height={600}
13        priority // Chargement prioritaire pour les images au-dessus de la ligne de flottaison
14      />
15
16      <Image
17        src="https://cdn.example.com/remote-image.jpg" // Image distante
18        alt="Remote Image"
19        width={800}
20        height={400}
21        quality={80} // Réglage de la qualité (1-100)
22        placeholder="blur"
23        blurDataURL="data:image/svg+xml;base64,..." // Placeholder optionnel pendant le chargement
24      />
25    </div>
26  );
27}

Principales fonctionnalités du composant Image :

  1. Tailles responsives automatiques - génération de différentes tailles d'image
  2. Lazy loading - les images ne sont chargées que lorsqu'elles sont visibles
  3. Conversion vers des formats modernes - WebP, AVIF
  4. Optimisation de la qualité - réduction de la taille du fichier tout en préservant la qualité
  5. Placeholder pendant le chargement - un flou (blur) qui évite les décalages de mise en page (CLS)

Optimisation des polices

L'App Router de Next.js introduit un nouveau système de chargement des polices qui élimine les problèmes de clignotement du texte (CLS) :

1// app/layout.tsx
2import { Inter, Roboto_Mono } from 'next/font/google';
3
4// Configuration d'une police variable (variable font)
5const inter = Inter({
6  subsets: ['latin', 'latin-ext'],
7  display: 'swap',
8});
9
10// Configuration d'une police à chasse fixe
11const robotoMono = Roboto_Mono({
12  subsets: ['latin'],
13  display: 'swap',
14  weight: ['400', '700'],
15});
16
17export default function RootLayout({ children }) {
18  return (
19    <html lang="pl" className={`${inter.className} ${robotoMono.variable}`}>
20      <body>{children}</body>
21    </html>
22  );
23}

Dans un composant, on peut ensuite utiliser la police définie :

1// app/page.tsx
2export default function Home() {
3  return (
4    <main>
5      <h1 className={inter.className}>Titre en police Inter</h1>
6      <code className="font-mono">// Ceci sera en Roboto Mono grâce à la variable CSS</code>
7    </main>
8  );
9}

Stratégies d'optimisation des données

React Server Components et réduction de la taille du bundle

Les React Server Components (RSC) permettent de réduire considérablement la quantité de JavaScript envoyée au navigateur :

1// app/products/page.tsx - Server Component (par défaut)
2async function getProducts() {
3  const res = await fetch('https://api.example.com/products');
4  return res.json();
5}
6
7export default async function ProductsPage() {
8  // Ce code n'est JAMAIS envoyé au client
9  const products = await getProducts();
10  const totalValue = products.reduce((sum, product) => sum + product.price, 0);
11
12  return (
13    <div>
14      <h1>Produits (valeur : {totalValue} €)</h1>
15      <ProductList products={products} />
16    </div>
17  );
18}

Streaming et Suspense

Le streaming permet d'envoyer l'interface progressivement, ce qui accélère le Time to First Byte et améliore l'interactivité :

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import Loading from './loading';
4
5// Composants qui chargent leurs données indépendamment les uns des autres
6import UserProfile from './UserProfile';
7import RecentOrders from './RecentOrders';
8import Analytics from './Analytics';
9
10export default function Dashboard() {
11  return (
12    <div className="dashboard">
13      <h1>Panneau de contrôle</h1>
14
15      <div className="dashboard-grid">
16        {/* Les éléments qui se chargent plus vite seront visibles immédiatement */}
17        <Suspense fallback={<Loading section="profile" />}>
18          <UserProfile />
19        </Suspense>
20
21        <Suspense fallback={<Loading section="orders" />}>
22          <RecentOrders />
23        </Suspense>
24
25        <Suspense fallback={<Loading section="analytics" />}>
26          <Analytics />
27        </Suspense>
28      </div>
29    </div>
30  );
31}

Optimisation du routage et de la navigation

Prefetching et Parallel Routes

L'App Router de Next.js précharge automatiquement les liens visibles à l'écran, et ce comportement peut être contrôlé plus finement :

1import Link from 'next/link';
2
3export default function Navigation() {
4  return (
5    <nav>
6      {/* Prefetch par défaut */}
7      <Link href="/products">Produits</Link>
8
9      {/* Prefetch désactivé */}
10      <Link href="/heavy-page" prefetch={false}>Page lourde</Link>
11
12      {/* Prefetch des en-têtes uniquement (pour les pages dynamiques) */}
13      <Link href="/blog/article-123">Article</Link>
14    </nav>
15  );
16}

Les Parallel Routes permettent de charger plusieurs sections d'une page indépendamment les unes des autres :

1// app/layout.tsx
2export default function Layout({ children, sidebar }) {
3  return (
4    <div className="layout">
5      <aside className="sidebar">
6        {sidebar}
7      </aside>
8      <main>
9        {children}
10      </main>
11    </div>
12  );
13}
14
15// app/@sidebar/page.tsx - chargé indépendamment
16export default function Sidebar() {
17  return <div>Contenu de la barre latérale</div>;
18}
19
20// app/page.tsx - contenu principal
21export default function Home() {
22  return <div>Contenu principal</div>;
23}

Route Handlers et optimisation des API

Les Route Handlers peuvent être optimisés pour la performance, en particulier en combinaison avec l'Edge Runtime :

1// app/api/optimized/route.ts
2import { NextResponse } from 'next/server';
3
4export const runtime = 'edge';
5
6export async function GET() {
7  // Application de CORS pour optimiser les requêtes preflight
8  const response = NextResponse.json({ data: 'valeur' });
9
10  response.headers.set('Access-Control-Allow-Origin', '*');
11  response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
12  response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
13
14  // Définition de Cache-Control pour le navigateur
15  response.headers.set(
16    'Cache-Control',
17    'public, max-age=60, s-maxage=3600, stale-while-revalidate=600'
18  );
19
20  return response;
21}

Techniques d'optimisation avancées

Bonnes pratiques pour la fonction generateStaticParams

La fonction generateStaticParams sert à générer des versions statiques de pages avec paramètres :

1// app/products/[category]/[id]/page.tsx
2export async function generateStaticParams() {
3  // Récupère uniquement les données nécessaires à la génération des chemins
4  const categories = await fetch('https://api.example.com/categories').then(r => r.json());
5
6  // Génère les combinaisons de paramètres pour les pages les plus importantes
7  const paths = [];
8
9  for (const category of categories) {
10    // Récupère uniquement les meilleurs produits de chaque catégorie
11    const topProducts = await fetch(
12      `https://api.example.com/categories/${category.id}/top-products`
13    ).then(r => r.json());
14
15    for (const product of topProducts) {
16      paths.push({
17        category: category.slug,
18        id: product.id.toString(),
19      });
20    }
21  }
22
23  return paths;
24}
25
26export default async function ProductPage({ params }) {
27  const { category, id } = await params;
28  // Page produit...
29}

L'optimisation consiste à :

  1. Générer uniquement les paramètres les plus importants (par ex. les produits les plus populaires)
  2. Réduire au minimum le nombre de requêtes pendant le build
  3. Mettre correctement en cache les données utilisées pour générer les paramètres

Optimisation des styles et du CSS

Next.js prend en charge différentes stratégies de chargement du CSS, que l'on peut optimiser :

1// app/products/[id]/page.tsx
2import './product-page.css'; // CSS global pour cette route
3
4// CSS Modules pour le composant
5import styles from './ProductDetails.module.css';
6
7// Styled JSX (option intégrée)
8function ProductColorChoice() {
9  return (
10    <div className="color-selector">
11      <style jsx>{`
12        .color-selector {
13          display: flex;
14          gap: 8px;
15        }
16        .color-option {
17          width: 24px;
18          height: 24px;
19          border-radius: 50%;
20          cursor: pointer;
21        }
22      `}</style>
23      <div className="color-option" style={{ backgroundColor: 'red' }} />
24      <div className="color-option" style={{ backgroundColor: 'blue' }} />
25    </div>
26  );
27}
28
29export default async function ProductPage({ params }) {
30  return (
31    <div className={styles.productContainer}>
32      <h1 className={styles.productTitle}>Détails du produit {(await params).id}</h1>
33      <ProductColorChoice />
34    </div>
35  );
36}

Optimisation du CSS :

  1. CSS Modules pour isoler les styles des composants
  2. Suppression automatique du CSS inutilisé (tree-shaking)
  3. Regroupement du CSS en chunks selon les routes de l'application

Tirer parti de HTTP/2 et HTTP/3

Héberger Next.js sur des plateformes qui prennent en charge les protocoles modernes HTTP/2 et HTTP/3 offre de meilleures performances grâce au :

  1. Multiplexage des requêtes - plusieurs requêtes sur une seule connexion
  2. Compression des en-têtes - moins de surcoût de communication
  3. Server Push - le serveur peut envoyer des ressources avant que le client ne les demande

Cela ne nécessite aucune modification du code, seulement une configuration adaptée du serveur ou le choix d'une plateforme d'hébergement appropriée (par ex. Vercel, Netlify, Cloudflare Pages).

Outils d'analyse et d'amélioration des performances

Next.js Analytics et Web Vitals

Next.js propose des métriques Web Vitals intégrées que l'on peut suivre :

1// instrumentation.ts (à la racine du projet)
2export function register() {
3  if (typeof window !== 'undefined') {
4    // Cette solution ne fonctionne que côté client
5    import('web-vitals').then(({ onCLS, onFID, onLCP, onTTFB }) => {
6      onCLS(metric => sendAnalyticsEvent('CLS', metric));
7      onFID(metric => sendAnalyticsEvent('FID', metric));
8      onLCP(metric => sendAnalyticsEvent('LCP', metric));
9      onTTFB(metric => sendAnalyticsEvent('TTFB', metric));
10    });
11  }
12}
13
14function sendAnalyticsEvent(name, metric) {
15  const body = JSON.stringify({
16    name,
17    value: metric.value,
18    page: window.location.pathname,
19    id: metric.id,
20  });
21
22  // Envoi vers ta propre API
23  navigator.sendBeacon('/api/analytics', body);
24}

Audit et analyse de la taille du bundle

L'analyse de la taille du bundle est essentielle pour la performance. Next.js propose une analyse du bundle intégrée :

1next build --analyze

On peut aussi utiliser des outils plus avancés comme @next/bundle-analyzer :

1// next.config.js
2const withBundleAnalyzer = require('@next/bundle-analyzer')({
3  enabled: process.env.ANALYZE === 'true',
4});
5
6module.exports = withBundleAnalyzer({
7  // Configuration de Next.js
8});

Ensuite, on lance :

1ANALYZE=true npm run build

Bonnes pratiques de sécurité dans Next.js

Dans la ville de Metropolis Quantum de l'an 2150, la sécurité n'est pas une option, c'est une nécessité. La moindre faille du système peut être exploitée par des cybercriminels ou des corporations hostiles. Dans cette section, nous allons voir les pratiques les plus importantes pour sécuriser une application Next.js, afin de protéger à la fois les données des utilisateurs et l'intégrité de tout le système.

Protection CSRF (Cross-Site Request Forgery)

Le CSRF est une attaque dans laquelle une page malveillante exploite la session authentifiée d'un utilisateur pour effectuer des actions non autorisées. Next.js propose des mécanismes de protection intégrés, en particulier dans les Server Actions.

Mettre en place la protection CSRF

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Vérification de l'en-tête Origin pour les mutations
7  if (request.method !== 'GET' && request.method !== 'HEAD') {
8    const origin = request.headers.get('origin');
9    const host = request.headers.get('host');
10
11    // Vérifie que l'Origin correspond à l'hôte
12    if (origin && !origin.includes(host || '')) {
13      return new NextResponse('Forbidden', { status: 403 });
14    }
15  }
16
17  return NextResponse.next();
18}
19
20export const config = {
21  matcher: '/api/:path*'
22};

Jeton CSRF pour les formulaires

1// lib/csrf.ts
2import { cookies } from 'next/headers';
3import crypto from 'crypto';
4
5export async function generateCSRFToken(): Promise<string> {
6  const token = crypto.randomBytes(32).toString('hex');
7  const cookieStore = await cookies();
8
9  cookieStore.set('csrf-token', token, {
10    httpOnly: true,
11    secure: process.env.NODE_ENV === 'production',
12    sameSite: 'strict',
13    maxAge: 60 * 60 // 1 heure
14  });
15
16  return token;
17}
18
19export async function verifyCSRFToken(token: string): Promise<boolean> {
20  const cookieStore = await cookies();
21  const storedToken = cookieStore.get('csrf-token')?.value;
22
23  return storedToken === token;
24}
25
26// app/actions.ts
27'use server';
28
29import { verifyCSRFToken } from '@/lib/csrf';
30
31export async function submitForm(formData: FormData) {
32  const csrfToken = formData.get('csrf-token') as string;
33
34  if (!await verifyCSRFToken(csrfToken)) {
35    throw new Error('Invalid CSRF token');
36  }
37
38  // Traitement du formulaire...
39}
40
41// app/my-form/page.tsx
42import { generateCSRFToken } from '@/lib/csrf';
43import { submitForm } from '@/app/actions';
44
45export default async function MyFormPage() {
46  const csrfToken = await generateCSRFToken();
47
48  return (
49    <form action={submitForm}>
50      <input type="hidden" name="csrf-token" value={csrfToken} />
51      {/* Autres champs du formulaire */}
52      <button type="submit">Envoyer</button>
53    </form>
54  );
55}

Prévention XSS (Cross-Site Scripting)

Le XSS est une attaque dans laquelle un attaquant injecte du code JavaScript malveillant dans l'application. Next.js et React offrent une protection intégrée, mais il vaut la peine de connaître des techniques supplémentaires.

La protection par défaut de React

React échappe automatiquement toutes les valeurs rendues dans le JSX :

1// Sûr - React échappe automatiquement
2function UserProfile({ username }: { username: string }) {
3  // Même si username = "<script>alert('xss')</script>"
4  // React l'affichera comme du texte, sans l'exécuter
5  return <div>Bonjour, {username} !</div>;
6}

Attention à dangerouslySetInnerHTML

1// DANGEREUX - peut exécuter du code malveillant
2function DangerousComponent({ html }: { html: string }) {
3  return <div dangerouslySetInnerHTML={{ __html: html }} />;
4}
5
6// SÛR - utilise une bibliothèque de nettoyage (sanitization)
7import DOMPurify from 'isomorphic-dompurify';
8
9function SafeComponent({ html }: { html: string }) {
10  const sanitized = DOMPurify.sanitize(html, {
11    ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
12    ALLOWED_ATTR: ['href', 'target']
13  });
14
15  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
16}

Content Security Policy (CSP)

La CSP est un mécanisme de protection contre le XSS qui consiste à définir quelles sources de code sont dignes de confiance :

1// next.config.js
2const ContentSecurityPolicy = `
3  default-src 'self';
4  script-src 'self' 'unsafe-eval' 'unsafe-inline' https://trusted-cdn.com;
5  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
6  img-src 'self' data: https: blob:;
7  font-src 'self' https://fonts.gstatic.com;
8  connect-src 'self' https://api.example.com;
9  frame-ancestors 'none';
10`;
11
12const securityHeaders = [
13  {
14    key: 'Content-Security-Policy',
15    value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
16  }
17];
18
19module.exports = {
20  async headers() {
21    return [
22      {
23        source: '/:path*',
24        headers: securityHeaders
25      }
26    ];
27  }
28};

Ou via le middleware :

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7
8  const csp = [
9    "default-src 'self'",
10    "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
11    "style-src 'self' 'unsafe-inline'",
12    "img-src 'self' data: https:",
13    "font-src 'self' data:",
14    "connect-src 'self' https://api.example.com"
15  ].join('; ');
16
17  response.headers.set('Content-Security-Policy', csp);
18
19  return response;
20}

Rate Limiting - protection contre les abus

Le rate limiting protège l'application contre les attaques de type DDoS et les abus d'API. On peut le mettre en place dans le Middleware de Next.js :

1// lib/rate-limit.ts
2import { LRUCache } from 'lru-cache';
3
4type RateLimitOptions = {
5  interval: number; // durée en millisecondes
6  uniqueTokenPerInterval: number; // nombre maximal de jetons uniques
7};
8
9export function rateLimit(options: RateLimitOptions) {
10  const tokenCache = new LRUCache({
11    max: options.uniqueTokenPerInterval || 500,
12    ttl: options.interval || 60000
13  });
14
15  return {
16    check: (limit: number, token: string): { success: boolean; remaining: number } => {
17      const tokenCount = (tokenCache.get(token) as number[]) || [0];
18
19      if (tokenCount[0] === 0) {
20        tokenCache.set(token, [1]);
21        return { success: true, remaining: limit - 1 };
22      }
23
24      const currentCount = tokenCount[0];
25
26      if (currentCount >= limit) {
27        return { success: false, remaining: 0 };
28      }
29
30      tokenCache.set(token, [currentCount + 1]);
31      return { success: true, remaining: limit - currentCount - 1 };
32    }
33  };
34}
35
36// middleware.ts
37import { NextResponse } from 'next/server';
38import type { NextRequest } from 'next/server';
39import { rateLimit } from '@/lib/rate-limit';
40
41const limiter = rateLimit({
42  interval: 60 * 1000, // 1 minute
43  uniqueTokenPerInterval: 500
44});
45
46export async function middleware(request: NextRequest) {
47  // Utilise l'IP comme identifiant
48  const ip = request.ip ?? 'anonymous';
49
50  try {
51    const { success, remaining } = limiter.check(10, ip); // 10 requêtes par minute
52
53    if (!success) {
54      return new NextResponse('Too Many Requests', {
55        status: 429,
56        headers: {
57          'X-RateLimit-Limit': '10',
58          'X-RateLimit-Remaining': '0',
59          'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
60        }
61      });
62    }
63
64    const response = NextResponse.next();
65    response.headers.set('X-RateLimit-Limit', '10');
66    response.headers.set('X-RateLimit-Remaining', remaining.toString());
67
68    return response;
69  } catch (error) {
70    return new NextResponse('Internal Server Error', { status: 500 });
71  }
72}
73
74export const config = {
75  matcher: '/api/:path*'
76};

Rate Limiting pour les Server Actions

1// lib/rate-limit-server-action.ts
2import { cookies } from 'next/headers';
3
4const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
5
6export async function checkRateLimit(
7  action: string,
8  limit: number = 10,
9  windowMs: number = 60000
10): Promise<boolean> {
11  const cookieStore = await cookies();
12  const sessionId = cookieStore.get('session-id')?.value || 'anonymous';
13  const key = `${sessionId}:${action}`;
14
15  const now = Date.now();
16  const record = rateLimitStore.get(key);
17
18  if (!record || now > record.resetAt) {
19    rateLimitStore.set(key, {
20      count: 1,
21      resetAt: now + windowMs
22    });
23    return true;
24  }
25
26  if (record.count >= limit) {
27    return false;
28  }
29
30  record.count++;
31  return true;
32}
33
34// app/actions.ts
35'use server';
36
37import { checkRateLimit } from '@/lib/rate-limit-server-action';
38
39export async function sensitiveAction(data: FormData) {
40  const allowed = await checkRateLimit('sensitive-action', 5, 60000);
41
42  if (!allowed) {
43    throw new Error('Rate limit exceeded. Please try again later.');
44  }
45
46  // Exécute l'action...
47}

En-têtes de sécurité (HSTS, X-Frame-Options, etc.)

Des en-têtes HTTP adaptés constituent le socle de la sécurité des applications web :

1// next.config.js
2const securityHeaders = [
3  {
4    key: 'X-DNS-Prefetch-Control',
5    value: 'on'
6  },
7  {
8    key: 'Strict-Transport-Security',
9    value: 'max-age=63072000; includeSubDomains; preload'
10  },
11  {
12    key: 'X-Frame-Options',
13    value: 'SAMEORIGIN'
14  },
15  {
16    key: 'X-Content-Type-Options',
17    value: 'nosniff'
18  },
19  {
20    key: 'X-XSS-Protection',
21    value: '1; mode=block'
22  },
23  {
24    key: 'Referrer-Policy',
25    value: 'strict-origin-when-cross-origin'
26  },
27  {
28    key: 'Permissions-Policy',
29    value: 'camera=(), microphone=(), geolocation=()'
30  }
31];
32
33module.exports = {
34  async headers() {
35    return [
36      {
37        source: '/:path*',
38        headers: securityHeaders
39      }
40    ];
41  }
42};

Ou via le middleware, pour davantage de contrôle :

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7
8  // HSTS - impose HTTPS
9  response.headers.set(
10    'Strict-Transport-Security',
11    'max-age=31536000; includeSubDomains'
12  );
13
14  // X-Frame-Options - protection contre le clickjacking
15  response.headers.set('X-Frame-Options', 'SAMEORIGIN');
16
17  // X-Content-Type-Options - empêche le MIME sniffing
18  response.headers.set('X-Content-Type-Options', 'nosniff');
19
20  // X-XSS-Protection - protection supplémentaire contre le XSS
21  response.headers.set('X-XSS-Protection', '1; mode=block');
22
23  // Referrer-Policy - contrôle des informations dans l'en-tête Referer
24  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
25
26  // Permissions-Policy - contrôle de l'accès aux API du navigateur
27  response.headers.set(
28    'Permissions-Policy',
29    'camera=(), microphone=(), geolocation=(), payment=()'
30  );
31
32  return response;
33}

Sécurité des API Routes et des Server Actions

Validation des entrées dans les Server Actions

1// app/actions.ts
2'use server';
3
4import { z } from 'zod';
5import { revalidatePath } from 'next/cache';
6
7const CreatePostSchema = z.object({
8  title: z.string().min(3).max(100),
9  content: z.string().min(10).max(5000),
10  tags: z.array(z.string()).max(5).optional()
11});
12
13export async function createPost(formData: FormData) {
14  // Validation des données d'entrée
15  const rawData = {
16    title: formData.get('title'),
17    content: formData.get('content'),
18    tags: formData.getAll('tags')
19  };
20
21  const validation = CreatePostSchema.safeParse(rawData);
22
23  if (!validation.success) {
24    return {
25      error: 'Validation failed',
26      issues: validation.error.issues
27    };
28  }
29
30  const { title, content, tags } = validation.data;
31
32  // Vérification de l'authentification
33  const session = await getServerSession();
34  if (!session?.user) {
35    return { error: 'Unauthorized' };
36  }
37
38  // Nettoyage du HTML (si l'on autorise la mise en forme)
39  const sanitizedContent = sanitizeHtml(content);
40
41  try {
42    const post = await db.post.create({
43      data: {
44        title,
45        content: sanitizedContent,
46        tags,
47        authorId: session.user.id
48      }
49    });
50
51    revalidatePath('/blog');
52
53    return { success: true, post };
54  } catch (error) {
55    console.error('Failed to create post:', error);
56    return { error: 'Failed to create post' };
57  }
58}

Des API Routes sécurisées

1// app/api/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth';
4import { z } from 'zod';
5
6const QuerySchema = z.object({
7  page: z.coerce.number().min(1).default(1),
8  limit: z.coerce.number().min(1).max(100).default(10)
9});
10
11export async function GET(request: NextRequest) {
12  try {
13    // Vérification de l'authentification
14    const session = await getServerSession();
15    if (!session) {
16      return NextResponse.json(
17        { error: 'Unauthorized' },
18        { status: 401 }
19      );
20    }
21
22    // Validation des paramètres de requête
23    const { searchParams } = new URL(request.url);
24    const validation = QuerySchema.safeParse({
25      page: searchParams.get('page'),
26      limit: searchParams.get('limit')
27    });
28
29    if (!validation.success) {
30      return NextResponse.json(
31        { error: 'Invalid parameters', issues: validation.error.issues },
32        { status: 400 }
33      );
34    }
35
36    const { page, limit } = validation.data;
37
38    // Requête sécurisée vers la base de données
39    const users = await db.user.findMany({
40      skip: (page - 1) * limit,
41      take: limit,
42      select: {
43        id: true,
44        name: true,
45        email: true,
46        // Ne renvoie JAMAIS le mot de passe ni d'autres données sensibles
47      }
48    });
49
50    return NextResponse.json({ users });
51  } catch (error) {
52    console.error('API Error:', error);
53    // Ne renvoie JAMAIS le détail des erreurs en production
54    return NextResponse.json(
55      { error: 'Internal server error' },
56      { status: 500 }
57    );
58  }
59}

Bonnes pratiques supplémentaires

1. Stocker les secrets en toute sécurité

1// .env.local (ne le commite JAMAIS dans le dépôt !)
2DATABASE_URL="postgresql://..."
3NEXTAUTH_SECRET="super-secret-key"
4API_KEY="secret-api-key"
5
6// app/config.ts
7const requiredEnvVars = [
8  'DATABASE_URL',
9  'NEXTAUTH_SECRET',
10  'API_KEY'
11] as const;
12
13// Vérification de la présence des variables d'environnement au démarrage
14requiredEnvVars.forEach((varName) => {
15  if (!process.env[varName]) {
16    throw new Error(`Missing required environment variable: ${varName}`);
17  }
18});
19
20export const config = {
21  database: {
22    url: process.env.DATABASE_URL!
23  },
24  auth: {
25    secret: process.env.NEXTAUTH_SECRET!
26  },
27  api: {
28    key: process.env.API_KEY!
29  }
30} as const;

2. Des cookies sécurisés

1// lib/cookies.ts
2import { cookies } from 'next/headers';
3
4export async function setSecureCookie(name: string, value: string) {
5  const cookieStore = await cookies();
6
7  cookieStore.set(name, value, {
8    httpOnly: true, // Inaccessible depuis JavaScript
9    secure: process.env.NODE_ENV === 'production', // HTTPS uniquement en production
10    sameSite: 'strict', // Protection contre le CSRF
11    maxAge: 60 * 60 * 24 * 7, // 7 jours
12    path: '/'
13  });
14}

3. Journalisation et monitoring

1// lib/logger.ts
2export function logSecurityEvent(event: {
3  type: 'auth_failure' | 'rate_limit' | 'csrf_violation' | 'xss_attempt';
4  ip: string;
5  userAgent: string;
6  details?: any;
7}) {
8  const logEntry = {
9    timestamp: new Date().toISOString(),
10    ...event
11  };
12
13  // En production, envoie vers un système de monitoring (Sentry, LogRocket, etc.)
14  if (process.env.NODE_ENV === 'production') {
15    // sendToMonitoring(logEntry);
16  }
17
18  console.warn('Security Event:', logEntry);
19}
20
21// middleware.ts
22export function middleware(request: NextRequest) {
23  const ip = request.ip ?? 'unknown';
24  const userAgent = request.headers.get('user-agent') ?? 'unknown';
25
26  // Journalise les activités suspectes
27  if (isSuspicious(request)) {
28    logSecurityEvent({
29      type: 'csrf_violation',
30      ip,
31      userAgent,
32      details: { url: request.url }
33    });
34  }
35
36  return NextResponse.next();
37}

Récapitulatif des bonnes pratiques de sécurité

À Metropolis Quantum, la sécurité est un processus continu, pas une action ponctuelle. Les principes clés :

  1. Ne fais jamais confiance aux entrées de l'utilisateur - valide et nettoie toujours
  2. Utilise HTTPS partout - impose-le avec HSTS
  3. Mets en place du rate limiting - protège-toi contre les abus
  4. Définis des en-têtes sécurisés - CSP, X-Frame-Options, etc.
  5. Protège-toi contre le CSRF - vérifie l'Origin, utilise des jetons
  6. Préviens le XSS - nettoie le HTML, utilise une CSP
  7. Stocke les secrets en sécurité - utilise des variables d'environnement
  8. Journalise les événements de sécurité - surveille les activités suspectes
  9. Mets régulièrement à jour tes dépendances - corrige les failles connues
  10. Applique le principe du moindre privilège - des permissions minimales

Récapitulatif : les meilleures pratiques d'optimisation des performances

Pour résumer, voici les principes clés de l'optimisation des performances dans Next.js :

  1. Utilise l'Edge Runtime pour les applications dynamiques et mondiales qui exigent une faible latence
  2. Tire parti des React Server Components pour réduire la quantité de JS envoyée au client
  3. Optimise les images grâce au composant Image de Next.js
  4. Mets en place le streaming avec le composant Suspense pour une interface plus réactive
  5. Gère la mise en cache à différents niveaux (navigateur, CDN, serveur)
  6. Profite du prefetching pour une navigation plus rapide entre les pages
  7. Surveille les Web Vitals et optimise en continu les chemins de rendu critiques
  8. Optimise le chargement des polices en utilisant le système de polices intégré de Next.js
  9. Élimine le JavaScript superflu en analysant la taille du bundle et en supprimant les dépendances inutilisées
  10. Teste sur de vrais appareils et avec des connexions plus lentes afin d'identifier les problèmes de performance

En appliquant ces pratiques, tu peux créer des applications Next.js à la fois fonctionnelles et ultra-rapides, qui offrent aux utilisateurs une excellente expérience, quels que soient leur localisation ou l'appareil qu'ils utilisent.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Edge Runtime et optimisation de la performance dans Next.js
4
5interface RuntimeInfo {
6  name: string;
7  runtime: 'nodejs' | 'edge';
8  latency: number; // ms
9  coldStart: number; // ms
10  regions: string[];
11  features: string[];
12}
13
14const runtimes: RuntimeInfo[] = [
15  {
16    name: 'Node.js Runtime',
17    runtime: 'nodejs',
18    latency: 120,
19    coldStart: 250,
20    regions: ['us-east-1'],
21    features: ['API Node.js complète', 'fs, crypto, stream', 'Aucune limite de temps', 'Gros paquets'],
22  },
23  {
24    name: 'Edge Runtime',
25    runtime: 'edge',
26    latency: 25,
27    coldStart: 5,
28    regions: ['Global CDN (30+ régions)'],
29    features: ['Web APIs', 'Cold start rapide', 'Faible latence', 'Limite 25MB'],
30  },
31];
32
33function LatencyBar({ value, max, color }: { value: number; max: number; color: string }) {
34  return (
35    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
36      <div style={{ flex: 1, height: 12, background: '#1a2744', borderRadius: 6, overflow: 'hidden' }}>
37        <div style={{
38          width: `${(value / max) * 100}%`, height: '100%',
39          background: color, borderRadius: 6, transition: 'width 0.5s ease'
40        }} />
41      </div>
42      <span style={{ color, fontSize: 12, minWidth: 50 }}>{value}ms</span>
43    </div>
44  );
45}
46
47export default function EdgeRuntimeDemo() {
48  const [selected, setSelected] = useState<RuntimeInfo>(runtimes[1]);
49  const [pings, setPings] = useState<number[]>([]);
50
51  const simulatePing = () => {
52    const base = selected.runtime === 'edge' ? 15 : 80;
53    const jitter = Math.floor(Math.random() * 40);
54    setPings(prev => [...prev.slice(-11), base + jitter]);
55  };
56
57  const avg = pings.length > 0 ? Math.round(pings.reduce((a, b) => a + b, 0) / pings.length) : 0;
58
59  return (
60    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
61      <h1 style={{ color: '#64ffda' }}>Edge Runtime et Performance</h1>
62
63      <div style={{ display: 'flex', gap: 12, margin: '16px 0' }}>
64        {runtimes.map(r => (
65          <button key={r.runtime} onClick={() => { setSelected(r); setPings([]); }} style={{
66            flex: 1, background: selected.runtime === r.runtime ? '#1565c0' : '#0d1117',
67            color: '#fff', border: `1px solid ${selected.runtime === r.runtime ? '#64ffda' : '#333'}`,
68            borderRadius: 8, padding: 16, cursor: 'pointer', fontFamily: 'monospace', textAlign: 'left'
69          }}>
70            <div style={{ color: '#64ffda', fontWeight: 'bold' }}>{r.name}</div>
71            <div style={{ fontSize: 11, color: '#888', marginTop: 4 }}>
72              {r.runtime === 'edge' ? "export const runtime = 'edge'" : "// runtime par défaut"}
73            </div>
74          </button>
75        ))}
76      </div>
77
78      <div style={{ background: '#0d1117', borderRadius: 8, padding: 16, marginBottom: 16 }}>
79        <h3 style={{ color: '#64ffda', margin: '0 0 12px' }}>Métriques</h3>
80        <div style={{ marginBottom: 8 }}>
81          <span style={{ color: '#888', fontSize: 12 }}>Latence :</span>
82          <LatencyBar value={selected.latency} max={150} color="#4caf50" />
83        </div>
84        <div style={{ marginBottom: 8 }}>
85          <span style={{ color: '#888', fontSize: 12 }}>Cold start:</span>
86          <LatencyBar value={selected.coldStart} max={300} color="#ff9800" />
87        </div>
88        <div style={{ marginTop: 12 }}>
89          <span style={{ color: '#888', fontSize: 12 }}>Régions : </span>
90          <span style={{ color: '#ce93d8', fontSize: 12 }}>{selected.regions.join(', ')}</span>
91        </div>
92        <div style={{ marginTop: 8 }}>
93          <span style={{ color: '#888', fontSize: 12 }}>Fonctionnalités :</span>
94          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 4 }}>
95            {selected.features.map(f => (
96              <span key={f} style={{
97                background: '#1a2744', border: '1px solid #333', borderRadius: 4,
98                padding: '2px 8px', fontSize: 11, color: '#aaa'
99              }}>{f}</span>
100            ))}
101          </div>
102        </div>
103      </div>
104
105      <button onClick={simulatePing} style={{
106        background: '#4caf50', color: '#fff', border: 'none', borderRadius: 6,
107        padding: '8px 16px', cursor: 'pointer', fontFamily: 'monospace'
108      }}>
109        Ping (simulation)
110      </button>
111      {pings.length > 0 && (
112        <span style={{ color: '#888', marginLeft: 12, fontSize: 12 }}>Moyenne : {avg}ms</span>
113      )}
114
115      <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 12 }}>
116        {pings.map((p, i) => (
117          <div key={i} style={{
118            background: p < 50 ? '#1b5e20' : p < 100 ? '#e65100' : '#b71c1c',
119            borderRadius: 4, padding: '2px 6px', fontSize: 11
120          }}>
121            {p}ms
122          </div>
123        ))}
124      </div>
125    </div>
126  );
127}

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. Le Partial Pre-rendering dans Next.js 16 combine :

  2. 2. L'Edge Runtime dans Next.js offre :

Voici 2 des 9 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un système de surveillance des performances : Core Web Vitals, benchmarks des stratégies de rendu

  • Tri vertical

    Place les étapes de traitement d'une requête SSR dans le bon ordre :

  • Tri vertical

    Place les étapes du Streaming SSR dans le bon ordre :

  • Tri horizontal

    Reconstitue la syntaxe qui force le rendu statique d'une page dans Next.js :

  • Tri vertical

    Place les couches de vérification du cache dans Next.js dans le bon ordre :

  • Tri horizontal

    Reconstitue la syntaxe de fetch avec mise en cache désactivée dans Next.js :

  • Tri vertical

    Place les étapes de génération d'une page statique dans Next.js dans le bon ordre :

  • Tri horizontal

    Reconstitue la syntaxe pour définir le temps de revalidation d'une page dans Next.js :

  • Clique dans l'ordre

    Reconstitue la syntaxe de revalidate dans fetch

  • Clique dans l'ordre

    Reconstitue l'export de loading.tsx

  • Tri horizontal

    Place les éléments dans le bon ordre : const Component -> = dynamic( -> () => import('./Component'), -> { ssr: false })

  • Tri vertical

    Place les éléments dans le bon ordre : User request arrives -> Server Component renders -> HTML sent to client -> Hydration on client

  • Tri horizontal

    Reconstitue la syntaxe d'appel de revalidation basée sur les tags dans Next.js :

  • Éditeur de code

    Crée un moteur de rendu complet : 1. Stratégie de rendu hybride - Server et Client Components 2. Mise en cache et revalidation - basée sur le temps et à la demande 3. Streaming et Suspense - chargement progressif 4. Surveillance des performances - Core Web Vitals 5. Edge Runtime - configuration optimale

Articles utiles