Cours Next.js · Module 8: Déploiement et production

Préparer l'application pour la production (build, analyze)

6 min de lecture
Dans cette leçon6

Préparer une application Next.js au déploiement en production est une étape cruciale du processus de développement, qui demande une attention particulière à l'optimisation, à l'analyse et à la configuration. Une application correctement préparée offrira une meilleure expérience utilisateur, de meilleures performances et une maintenance plus facile.

Le processus de build d'une application Next.js

Le processus de build dans Next.js transforme le code source en une version optimisée prête au déploiement. Pendant ce processus, Next.js :

  1. Compile le code React et TypeScript en JavaScript
  2. Minifie le code CSS et JavaScript
  3. Optimise les images
  4. Génère des pages statiques (pour getStaticProps)
  5. Prépare les composants serveur (pour getServerSideProps)
  6. Crée les fichiers nécessaires à la navigation côté client

Le processus de build de base

Pour builder une application Next.js, suis ces étapes :

1# D'abord, assure-toi que toutes les dépendances sont installées
2npm install
3
4# Ensuite, builde l'application
5npm run build

Une fois le processus de build terminé, Next.js affiche un récapitulatif des pages, de leurs types (statique, SSR, API) et de la taille de chaque page. Ce récapitulatif fournit des informations précieuses sur les performances de l'application.

Lancer l'application buildée

Après avoir buildé l'application avec succès, tu peux lancer la version de production en local avec :

1npm run start

Cette commande démarre un serveur Node.js qui sert ton application Next.js en mode production, sans hot-reloading et avec des assets optimisés.

Analyse du bundle (bundle analysis)

L'analyse de la taille du bundle est essentielle pour comprendre exactement ce qui influence la taille de ton application. Next.js propose un outil d'analyse de bundle intégré que tu peux activer à l'aide d'une variable d'environnement :

1ANALYZE=true npm run build

Pour utiliser cette fonctionnalité, tu dois installer et configurer l'outil @next/bundle-analyzer :

1npm install --save-dev @next/bundle-analyzer

Puis mets à jour le fichier next.config.js :

1const withBundleAnalyzer = require('@next/bundle-analyzer')({
2  enabled: process.env.ANALYZE === 'true',
3})
4
5module.exports = withBundleAnalyzer({
6  // Place ici les autres options de configuration de Next.js
7})

Après avoir lancé le build avec l'analyzer activé, des rapports visuels seront générés, montrant une décomposition détaillée de la taille des bundles client et serveur.

Interpréter les résultats de l'analyse du bundle

L'analyse du bundle révèle :

  1. Grosses dépendances - les bibliothèques qui influencent fortement la taille du bundle
  2. Doublons - les mêmes paquets chargés plusieurs fois
  3. Code inutilisé - du code mort qui peut être supprimé
  4. Répartition des modules - quelles parties de l'application occupent le plus d'espace

À partir de ces informations, tu peux prendre des décisions d'optimisation éclairées :

  • Remplacer les grosses bibliothèques par des alternatives plus légères
  • Implémenter le lazy loading pour les gros composants
  • Supprimer les dépendances inutilisées
  • Découper le code en plus petits chunks

Optimisation des performances

Code splitting

Next.js implémente automatiquement le code splitting, mais tu peux optimiser davantage ce processus grâce aux imports dynamiques :

1import dynamic from 'next/dynamic';
2
3// Au lieu d'importer directement
4// import HeavyComponent from '../components/HeavyComponent';
5
6// Utilise un import dynamique
7const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
8  loading: () => <p>Loading...</p>,
9  // Le SSR peut être désactivé pour les composants inutiles côté serveur
10  ssr: false,
11});
12
13export default function HomePage() {
14  return (
15    <div>
16      <h1>Home page</h1>
17      <HeavyComponent />
18    </div>
19  );
20}

Optimisation des images

Next.js propose le composant Image qui optimise automatiquement les images, mais il vaut la peine d'en configurer en plus les options :

1import Image from 'next/image';
2
3export default function ProductCard({ product }) {
4  return (
5    <div>
6      <Image
7        src={product.imageUrl}
8        alt={product.name}
9        width={300}
10        height={200}
11        placeholder="blur"
12        blurDataURL={product.thumbnailUrl}
13        priority={product.featured} // Charge l'image en priorité haute si elle est mise en avant
14      />
15      <h2>{product.name}</h2>
16    </div>
17  );
18}

Tree shaking

Le tree shaking est une technique d'élimination du code mort pendant le processus de build. Next.js utilise Webpack, qui effectue le tree shaking automatiquement. Pour tirer le meilleur parti du tree shaking :

  1. Utilise des imports ES Module plutôt que require
  2. Évite les imports à effet de bord (par ex. import 'library')
  3. Préfère les imports nommés aux imports par défaut quand tu n'as besoin que d'une partie d'une bibliothèque :
1// Mauvais : importe toute la bibliothèque
2import _ from 'lodash';
3const sortedItems = _.sortBy(items, 'name');
4
5// Bon : importe uniquement la fonction nécessaire
6import { sortBy } from 'lodash-es';
7const sortedItems = sortBy(items, 'name');

Variables d'environnement

Next.js gère les variables d'environnement via les fichiers .env, .env.local, .env.development et .env.production. Elles sont essentielles pour configurer l'application dans différents environnements.

1# .env.production
2NEXT_PUBLIC_API_URL=https://api.twojadomena.com
3DATABASE_URL=postgresql://user:password@production-db:5432/mydb
4FEATURE_FLAGS_ENABLED=true

Les variables préfixées par NEXT_PUBLIC_ sont disponibles dans le navigateur, tandis que les autres ne sont visibles que côté serveur.

Gestion sécurisée des secrets

Pour les secrets de production :

  1. Ne stocke jamais de secrets dans le dépôt
  2. Utilise la gestion des secrets de la plateforme d'hébergement (Vercel, Netlify)
  3. Pour les opérations sensibles, privilégie le code côté serveur (API Routes)
1// pages/api/secure-operation.ts
2export default async function handler(req, res) {
3  // API_SECRET n'est pas accessible dans le code client
4  const apiSecret = process.env.API_SECRET;
5
6  // Exécution sécurisée de l'opération avec le secret
7  const result = await performSecureOperation(apiSecret);
8
9  // Ne renvoie que des données sûres
10  res.status(200).json({ success: true, data: result.safeData });
11}

Tests de production

Avant le déploiement, lance des tests sur la version de production :

  1. Tests de performance - utilise Lighthouse ou WebPageTest
  2. Tests fonctionnels - vérifie que toutes les fonctionnalités marchent comme prévu
  3. Tests de réactivité - vérifie différentes tailles d'écran
  4. Tests navigateurs - vérifie la compatibilité avec différents navigateurs

Automatiser les tests avant la mise en production

Mets en place un script qui lancera tous les tests nécessaires :

1// scripts/pre-deploy-checks.js
2const { execSync } = require('child_process');
3
4// Lance les tests unitaires
5console.log('Running unit tests...');
6execSync('npm run test', { stdio: 'inherit' });
7
8// Lance les tests end-to-end
9console.log('Running E2E tests...');
10execSync('npm run test:e2e', { stdio: 'inherit' });
11
12// Vérifie les types
13console.log('Checking types...');
14execSync('npm run typecheck', { stdio: 'inherit' });
15
16// Vérifie le linting
17console.log('Vérification du linting...');
18execSync('npm run lint', { stdio: 'inherit' });
19
20// Builde l'application
21console.log("Build de l'application...");
22execSync('npm run build', { stdio: 'inherit' });
23
24console.log('Toutes les vérifications de pré-déploiement sont passées avec succès !');

Résumé

Préparer une application Next.js pour la production est un processus en plusieurs étapes qui comprend :

  1. Le build d'une version optimisée de l'application
  2. L'analyse du bundle pour repérer les opportunités d'optimisation
  3. L'implémentation de techniques pour améliorer les performances
  4. La configuration des variables d'environnement
  5. La gestion sécurisée des secrets
  6. Les tests de production

Un processus de préparation à la production mené avec soin garantit de meilleures performances, une meilleure sécurité et une meilleure expérience utilisateur, ce qui se traduit par le succès commercial du projet.

N'oublie pas que l'optimisation est un processus continu - un monitoring régulier des performances et la mise à jour de l'application selon les nouvelles bonnes pratiques sont la clé d'une application performante.

Code de cette leçon : App.tsx
1// Demo: Preparing Next.js application for production
2// Build process, optimization, deployment checklist
3import React, { useState } from 'react';
4
5// Processus de build de Next.js :
6// 1. npm run build → next build
7//    - Compiles TypeScript
8//    - Generates static pages (SSG)
9//    - Optimizes bundle (tree shaking, code splitting)
10//    - Generates sitemap, robots.txt
11// 2. npm run start → next start
12//    - Starts production server
13//    - Serves static files from .next/
14//    - Handles SSR and ISR
15
16interface ChecklistItem {
17  id: string;
18  category: string;
19  title: string;
20  description: string;
21  impact: 'critical' | 'high' | 'medium';
22  command?: string;
23}
24
25const checklist: ChecklistItem[] = [
26  {
27    id: '1', category: 'Build', title: 'Build without errors',
28    description: 'npm run build must complete successfully',
29    impact: 'critical', command: 'npm run build',
30  },
31  {
32    id: '2', category: 'Build', title: 'TypeScript - no type errors',
33    description: 'Strict mode, zero any warnings',
34    impact: 'critical', command: 'npx tsc --noEmit',
35  },
36  {
37    id: '3', category: 'Build', title: 'Linting',
38    description: 'ESLint with no warnings or errors',
39    impact: 'high', command: 'npm run lint',
40  },
41  {
42    id: '4', category: 'Performance', title: 'Bundle analysis',
43    description: 'Check bundle size, remove unused dependencies',
44    impact: 'high', command: 'ANALYZE=true npm run build',
45  },
46  {
47    id: '5', category: 'Performance', title: 'Image optimization',
48    description: 'Use next/image, WebP/AVIF, lazy loading',
49    impact: 'high',
50  },
51  {
52    id: '6', category: 'Performance', title: 'Font optimization',
53    description: 'Use next/font, self-hosting, font-display: swap',
54    impact: 'medium',
55  },
56  {
57    id: '7', category: 'SEO', title: 'Metadata on every page',
58    description: 'title, description, Open Graph pour le social sharing',
59    impact: 'high',
60  },
61  {
62    id: '8', category: 'SEO', title: 'sitemap.xml et robots.txt',
63    description: 'Automatic generation in app/sitemap.ts and app/robots.ts',
64    impact: 'medium',
65  },
66  {
67    id: '9', category: 'Security', title: 'Environment variables',
68    description: 'Secrets in .env.local, NEXT_PUBLIC_ only for public ones',
69    impact: 'critical',
70  },
71  {
72    id: '10', category: 'Security', title: 'Security headers',
73    description: 'CSP, X-Frame-Options, HSTS dans next.config.js',
74    impact: 'high',
75  },
76  {
77    id: '11', category: 'Deploy', title: 'Platform selection',
78    description: 'Vercel (best), Netlify, AWS, Docker',
79    impact: 'high',
80  },
81  {
82    id: '12', category: 'Deploy', title: 'CI/CD pipeline',
83    description: 'GitHub Actions: build → test → deploy',
84    impact: 'medium',
85  },
86];
87
88const impactColors = { critical: '#f44336', high: '#ff9800', medium: '#64ffda' };
89const categories = [...new Set(checklist.map(c => c.category))];
90
91function BuildOutput() {
92  const routes = [
93    { path: '/', type: 'Static', size: '5.2 kB' },
94    { path: '/products', type: 'SSG', size: '12.4 kB' },
95    { path: '/products/[id]', type: 'SSG', size: '8.7 kB' },
96    { path: '/dashboard', type: 'SSR', size: '15.1 kB' },
97    { path: '/api/health', type: 'API', size: '1.2 kB' },
98  ];
99
100  const typeIcons: Record<string, string> = {
101    Static: 'S', SSG: 'G', SSR: 'R', API: 'A',
102  };
103  const typeColors: Record<string, string> = {
104    Static: '#7c4dff', SSG: '#64ffda', SSR: '#ff9800', API: '#2196f3',
105  };
106
107  return (
108    <div style={{
109      background: 'rgba(0,0,0,0.3)', borderRadius: '8px', padding: '16px',
110    }}>
111      <h4 style={{ color: '#64ffda', marginBottom: '8px', fontSize: '0.85rem' }}>
112        $ next build
113      </h4>
114      <div style={{ display: 'grid', gap: '4px' }}>
115        {routes.map(r => (
116          <div key={r.path} style={{
117            display: 'flex', alignItems: 'center', gap: '10px',
118            padding: '6px 8px', fontFamily: 'monospace', fontSize: '0.8rem',
119          }}>
120            <span style={{
121              width: '20px', height: '20px', borderRadius: '4px',
122              background: typeColors[r.type], color: '#0f0f23',
123              display: 'flex', alignItems: 'center', justifyContent: 'center',
124              fontSize: '0.7rem', fontWeight: 'bold', flexShrink: 0,
125            }}>{typeIcons[r.type]}</span>
126            <span style={{ color: '#b0bec5', flex: 1 }}>{r.path}</span>
127            <span style={{ color: '#78909c' }}>{r.size}</span>
128          </div>
129        ))}
130      </div>
131      <div style={{
132        marginTop: '12px', padding: '8px', borderTop: '1px solid rgba(255,255,255,0.05)',
133        display: 'flex', justifyContent: 'space-between',
134      }}>
135        <span style={{ color: '#4caf50', fontSize: '0.8rem' }}>Build completed in 12.4s</span>
136        <span style={{ color: '#78909c', fontSize: '0.8rem' }}>Total: 42.6 kB</span>
137      </div>
138    </div>
139  );
140}
141
142export default function ProductionChecklist() {
143  const [completed, setCompleted] = useState<Set<string>>(new Set());
144  const [activeCategory, setActiveCategory] = useState('all');
145
146  const toggle = (id: string) => {
147    setCompleted(prev => {
148      const next = new Set(prev);
149      if (next.has(id)) next.delete(id); else next.add(id);
150      return next;
151    });
152  };
153
154  const filtered = activeCategory === 'all'
155    ? checklist
156    : checklist.filter(c => c.category === activeCategory);
157
158  const progress = Math.round((completed.size / checklist.length) * 100);
159
160  return (
161    <div style={{
162      background: '#0f0f23', minHeight: '100vh', padding: '24px',
163      color: '#fff', fontFamily: 'system-ui, sans-serif',
164    }}>
165      <h1 style={{ color: '#64ffda', marginBottom: '4px' }}>Production Checklist</h1>
166      <p style={{ color: '#b0bec5', marginBottom: '16px' }}>
167        Preparing Next.js application for deployment
168      </p>
169
170      <div style={{
171        background: 'rgba(100,255,218,0.05)', borderRadius: '10px',
172        padding: '12px 16px', marginBottom: '20px',
173        display: 'flex', alignItems: 'center', gap: '16px',
174      }}>
175        <div style={{
176          width: '48px', height: '48px', borderRadius: '50%',
177          background: `conic-gradient(#64ffda ${progress * 3.6}deg, rgba(255,255,255,0.05) 0deg)`,
178          display: 'flex', alignItems: 'center', justifyContent: 'center',
179        }}>
180          <span style={{ color: '#64ffda', fontWeight: 'bold', fontSize: '0.8rem' }}>{progress}%</span>
181        </div>
182        <div>
183          <p style={{ color: '#fff', margin: 0, fontWeight: 600 }}>{completed.size}/{checklist.length} completed</p>
184          <p style={{ color: '#78909c', margin: 0, fontSize: '0.8rem' }}>
185            {progress === 100 ? 'Ready to deploy!' : 'Complete all checks before deploying'}
186          </p>
187        </div>
188      </div>
189
190      <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
191        <button onClick={() => setActiveCategory('all')} style={{
192          background: activeCategory === 'all' ? '#64ffda' : 'transparent',
193          color: activeCategory === 'all' ? '#0f0f23' : '#b0bec5',
194          border: '1px solid rgba(100,255,218,0.3)',
195          padding: '6px 12px', borderRadius: '20px', cursor: 'pointer',
196        }}>All</button>
197        {categories.map(c => (
198          <button key={c} onClick={() => setActiveCategory(c)} style={{
199            background: activeCategory === c ? '#64ffda' : 'transparent',
200            color: activeCategory === c ? '#0f0f23' : '#b0bec5',
201            border: '1px solid rgba(100,255,218,0.3)',
202            padding: '6px 12px', borderRadius: '20px', cursor: 'pointer',
203          }}>{c}</button>
204        ))}
205      </div>
206
207      <div style={{ display: 'grid', gap: '8px', marginBottom: '20px' }}>
208        {filtered.map(item => (
209          <div key={item.id} onClick={() => toggle(item.id)} style={{
210            display: 'flex', alignItems: 'center', gap: '12px',
211            background: completed.has(item.id) ? 'rgba(76,175,80,0.05)' : 'rgba(255,255,255,0.02)',
212            border: `1px solid ${completed.has(item.id) ? 'rgba(76,175,80,0.2)' : 'rgba(255,255,255,0.06)'}`,
213            borderRadius: '8px', padding: '12px', cursor: 'pointer',
214          }}>
215            <div style={{
216              width: '22px', height: '22px', borderRadius: '6px',
217              border: `2px solid ${completed.has(item.id) ? '#4caf50' : 'rgba(255,255,255,0.2)'}`,
218              background: completed.has(item.id) ? '#4caf50' : 'transparent',
219              display: 'flex', alignItems: 'center', justifyContent: 'center',
220              color: '#fff', fontSize: '0.7rem', flexShrink: 0,
221            }}>
222              {completed.has(item.id) ? 'V' : ''}
223            </div>
224            <div style={{ flex: 1 }}>
225              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
226                <span style={{
227                  color: completed.has(item.id) ? '#78909c' : '#fff',
228                  textDecoration: completed.has(item.id) ? 'line-through' : 'none',
229                }}>{item.title}</span>
230                <span style={{
231                  background: `${impactColors[item.impact]}15`,
232                  color: impactColors[item.impact],
233                  padding: '1px 8px', borderRadius: '10px', fontSize: '0.65rem',
234                }}>{item.impact}</span>
235              </div>
236              <p style={{ color: '#78909c', fontSize: '0.8rem', margin: '2px 0 0' }}>{item.description}</p>
237            </div>
238          </div>
239        ))}
240      </div>
241
242      <BuildOutput />
243    </div>
244  );
245}

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. La commande « npm run build » dans Next.js :

  2. 2. Le bundle analyzer dans Next.js sert à :

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 une interface avec optimisation de la taille du bundle et lazy loading

  • Clique dans l'ordre

    Place les éléments dans le bon ordre : npm → install → @next/bundle-analyzer

Articles utiles