Cours JavaScript et TypeScript · Module 9: Design patterns et architecture

Optimisation du bundle - analyse et optimisation des paquets

19 min de lecture
Dans cette leçon9

Imagine un camion qui apporte au parc un seul sac de nourriture, mais qui traîne tout l'entrepôt derrière lui, parce que personne n'a vérifié le chargement. L'optimisation du bundle est un processus complet d'analyse et d'optimisation des paquets JavaScript : une taille réduite, un chargement plus rapide et une utilisation efficace des ressources du navigateur. C'est la dernière étape après le code splitting, le tree shaking et le lazy loading.

Pourquoi est-ce important ?

Les chiffres ci-dessous sont un exemple hypothétique, pas le résultat d'une étude, mais ils montrent ce que l'on mesure :

1// Exemple hypothétique : des chiffres illustratifs, pas des résultats de mesures
2
3// Métriques de performance avant l'optimisation
4const beforeOptimization = {
5  bundleSize: '2.8MB',
6  firstContentfulPaint: '3.4s',
7  largestContentfulPaint: '5.1s',
8  timeToInteractive: '6.2s',
9  cumulativeLayoutShift: 0.25
10};
11
12// Métriques de performance après l'optimisation
13const afterOptimization = {
14  bundleSize: '420KB',
15  firstContentfulPaint: '0.9s',
16  largestContentfulPaint: '1.6s',
17  timeToInteractive: '2.1s',
18  cumulativeLayoutShift: 0.05
19};
20
21// Amélioration des performances
22const improvement = {
23  bundleSizeReduction: '85%',
24  fcpImprovement: '73%',
25  lcpImprovement: '69%',
26  ttiImprovement: '66%',
27  clsImprovement: '80%'
28};
29
30// Analyse des coûts : business impact
31const businessImpact = {
32  conversionRate: {
33    before: '2.1%',
34    after: '3.8%',
35    improvement: '+81%'
36  },
37  bounceRate: {
38    before: '68%',
39    after: '42%',
40    improvement: '-38%'
41  },
42  mobileUsersLost: {
43    before: '35%', // À cause du chargement lent
44    after: '12%',
45    improvement: '-66%'
46  },
47  revenueImpact: {
48    monthlyLoss: '$24,000', // À cause des bundles lents
49    monthlyGain: '$45,000'  // Après l'optimisation
50  }
51};

Parmi ces métriques, LCP et CLS font partie des Core Web Vitals (la troisième est INP), FCP est une métrique complémentaire, et TTI n'entre plus dans le score depuis Lighthouse 10. Mesure toujours tes propres chiffres.

Les outils d'analyse des bundles

Tu mets en place le code splitting en quatre étapes : analyse de la taille du bundle, choix des points de découpe (split points), configuration des imports dynamiques et vérification du chargement des chunks.

Webpack Bundle Analyzer

Webpack Bundle Analyzer dessine une carte des modules et montre lesquels prennent le plus de place :

1# Installation et utilisation
2npm install --save-dev webpack-bundle-analyzer
3
4# Génération du rapport (l'analyzer lit le fichier de statistiques de webpack)
5npx webpack --profile --json=stats.json
6npx webpack-bundle-analyzer stats.json
7
8# Intégration dans la config webpack
9const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
10
11module.exports = {
12  plugins: [
13    new BundleAnalyzerPlugin({
14      analyzerMode: 'static',
15      openAnalyzer: false,
16      generateStatsFile: true,
17      reportFilename: 'bundle-report.html'
18    })
19  ]
20};

L'analyzer lit le fichier de statistiques (stats.json), et non les fichiers .js finaux.

Analyse dans la CI/CD

Un workflow GitHub Actions vérifie la taille à chaque push et à chaque pull request :

1# .github/workflows/bundle-analysis.yml
2name: Bundle Analysis
3on: [push, pull_request]
4
5jobs:
6  analyze:
7    runs-on: ubuntu-latest
8    steps:
9      - uses: actions/checkout@v4
10
11      - name: Setup Node.js
12        uses: actions/setup-node@v4
13        with:
14          node-version: '22'
15
16      - name: Install dependencies
17        run: npm ci
18
19      - name: Build and analyze
20        run: |
21          npm run build
22          npx size-limit
23
24      - name: Upload bundle stats
25        uses: actions/upload-artifact@v4
26        with:
27          name: bundle-stats
28          path: dist/stats.json

npx size-limit (avec les paquets size-limit et @size-limit/file dans les devDependencies) interrompt le build dès qu'un fichier dépasse la limite définie dans package.json.

Source Map Explorer

Source Map Explorer montre de quels fichiers sources se compose le bundle final :

1# Analyse de la source map
2npm install --global source-map-explorer
3
4# Analyse d'un bundle précis
5source-map-explorer bundle.js bundle.js.map
6
7# Rapport HTML (conserve-le pour le comparer avec la version suivante)
8source-map-explorer bundle.js --html report.html

L'outil ne compare pas les versions, alors conserve les rapports HTML d'une release à l'autre.

Webpack Dashboard

Webpack Dashboard transforme le log brut de webpack en un panneau lisible dans le terminal :

1// Dashboard interactif pendant le développement
2const DashboardPlugin = require('webpack-dashboard/plugin');
3
4module.exports = {
5  plugins: [
6    new DashboardPlugin()
7  ]
8};

Le panneau s'affiche quand tu lances webpack via la CLI du paquet, par exemple webpack-dashboard -- webpack --watch. Tu vois alors la taille des modules à chaque modification.

Techniques d'optimisation avancées

Stratégies de découpage en chunks

splitChunks découpe node_modules en groupes (cache groups) que le navigateur met en cache indépendamment du code de l'application :

1// webpack.config.js - configuration avancée des chunks
2module.exports = {
3  optimization: {
4    splitChunks: {
5      chunks: 'all',
6      minSize: 20000,
7      maxSize: 244000,
8      cacheGroups: {
9        // Framework chunks (React, Angular, Vue)
10        framework: {
11          test: /[\\/]node_modules[\\/](react|react-dom|@angular|vue)[\\/]/,
12          name: 'framework',
13          priority: 40,
14          enforce: true
15        },
16
17        // UI Libraries chunk
18        ui: {
19          test: /[\\/]node_modules[\\/](@mui|antd|bootstrap|@chakra-ui)[\\/]/,
20          name: 'ui-libs',
21          priority: 30,
22          enforce: true
23        },
24
25        // Utility libraries chunk
26        utils: {
27          test: /[\\/]node_modules[\\/](lodash|ramda|date-fns|moment)[\\/]/,
28          name: 'utils',
29          priority: 20,
30          enforce: true
31        },
32
33        // Common application code
34        common: {
35          name: 'common',
36          minChunks: 2,
37          priority: 10,
38          reuseExistingChunk: true
39        },
40
41        // Les grosses bibliothèques dans des chunks séparés
42        largeDeps: {
43          test: /[\\/]node_modules[\\/](monaco-editor|three|d3)[\\/]/,
44          name(module) {
45            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
46            return `lib.${packageName.replace('@', '')}`;
47          },
48          priority: 35,
49          enforce: true
50        }
51      }
52    }
53  }
54};

Le priority le plus élevé l'emporte en cas de conflit entre groupes, et enforce: true ignore les seuils de taille. Quand ton code change, l'utilisateur ne retélécharge pas React.

Module Federation pour les micro-frontends

Module Federation, intégré à webpack 5, permet à des applications indépendantes de partager du code pendant l'exécution :

1// webpack.config.js - Host application
2const { ModuleFederationPlugin } = require('webpack').container;
3
4module.exports = {
5  plugins: [
6    new ModuleFederationPlugin({
7      name: 'host',
8      filename: 'remoteEntry.js',
9      remotes: {
10        userDashboard: 'userDashboard@http://localhost:3001/remoteEntry.js',
11        productCatalog: 'productCatalog@http://localhost:3002/remoteEntry.js',
12        paymentSystem: 'paymentSystem@http://localhost:3003/remoteEntry.js'
13      },
14      shared: {
15        react: {
16          singleton: true,
17          requiredVersion: '^19.0.0',
18          eager: true
19        },
20        'react-dom': {
21          singleton: true,
22          requiredVersion: '^19.0.0',
23          eager: true
24        }
25      }
26    })
27  ]
28};
29
30// Micro-frontend - User Dashboard
31const { ModuleFederationPlugin } = require('webpack').container;
32
33module.exports = {
34  plugins: [
35    new ModuleFederationPlugin({
36      name: 'userDashboard',
37      filename: 'remoteEntry.js',
38      exposes: {
39        './UserProfile': './src/components/UserProfile',
40        './UserSettings': './src/components/UserSettings'
41      },
42      shared: {
43        react: { singleton: true },
44        'react-dom': { singleton: true }
45      }
46    })
47  ]
48};

singleton: true garantit une seule copie de React, et les deux module.exports correspondent à deux fichiers de configuration distincts.

Build incrémental

Le cache sur disque raccourcit les builds suivants, car webpack ne reconstruit que ce qui a changé :

1// Configuration du cache webpack pour des rebuilds plus rapides
2const path = require('path');
3
4module.exports = {
5  cache: {
6    type: 'filesystem',
7    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
8    buildDependencies: {
9      config: [__filename],
10      tsconfig: [path.resolve(__dirname, 'tsconfig.json')],
11      packagejson: [path.resolve(__dirname, 'package.json')]
12    },
13    version: '1.0',
14    store: 'pack',
15    compression: 'gzip'
16  },
17
18  // Cache persistant pour node_modules
19  snapshot: {
20    managedPaths: [path.resolve(__dirname, 'node_modules')],
21    buildDependencies: {
22      hash: true,
23      timestamp: true
24    },
25    module: {
26      timestamp: true
27    },
28    resolve: {
29      timestamp: true
30    }
31  }
32};

buildDependencies indique les fichiers dont la modification invalide le cache.

Budgets et monitoring

Configuration du budget

Un budget de performance (performance budget) est une limite de taille que l'outil size-limit lit dans package.json :

1{
2  "size-limit": [
3    {
4      "path": "./dist/js/main.*.js",
5      "limit": "170 kB",
6      "gzip": true
7    },
8    {
9      "path": "./dist/js/vendor.*.js",
10      "limit": "300 kB",
11      "gzip": true
12    },
13    {
14      "path": "./dist/css/*.css",
15      "limit": "50 kB",
16      "gzip": true
17    },
18    {
19      "path": "./dist/js/runtime.*.js",
20      "limit": "10 kB",
21      "gzip": true
22    }
23  ]
24}

Les limites portent sur la taille après gzip. Supprime la première ligne, car le JSON n'accepte pas les commentaires.

Monitoring automatisé des performances

Le moniteur collecte les temps de chargement via la Performance API chez de vrais utilisateurs et les compare à des seuils :

1// Performance monitoring service
2class BundlePerformanceMonitor {
3  constructor() {
4    this.metrics = {
5      bundleSizes: new Map(),
6      loadTimes: new Map(),
7      cacheHitRates: new Map()
8    };
9
10    this.thresholds = {
11      maxBundleSize: 250000, // 250KB
12      maxLoadTime: 3000,     // 3s
13      minCacheHitRate: 80    // 80%
14    };
15  }
16
17  async measureBundlePerformance() {
18    const performanceData = {
19      timestamp: Date.now(),
20      metrics: await this.collectMetrics(),
21      violations: this.checkViolations(),
22      recommendations: this.generateRecommendations()
23    };
24
25    await this.sendToMonitoring(performanceData);
26    return performanceData;
27  }
28
29  async collectMetrics() {
30    const bundleEntries = performance.getEntriesByType('navigation');
31    const resourceEntries = performance.getEntriesByType('resource');
32
33    return {
34      pageLoadTime: bundleEntries[0]?.loadEventEnd - bundleEntries[0]?.fetchStart,
35      resourceLoadTimes: resourceEntries
36        .filter(entry => entry.name.includes('.js') || entry.name.includes('.css'))
37        .map(entry => ({
38          name: entry.name,
39          size: entry.transferSize,
40          loadTime: entry.responseEnd - entry.fetchStart,
41          cached: entry.transferSize === 0
42        })),
43      memoryUsage: performance.memory ? {
44        used: performance.memory.usedJSHeapSize,
45        total: performance.memory.totalJSHeapSize,
46        limit: performance.memory.jsHeapSizeLimit
47      } : null
48    };
49  }
50
51  checkViolations() {
52    const violations = [];
53
54    // Vérification de la taille des bundles (transferSize issu de Resource Timing)
55    performance.getEntriesByType('resource')
56      .filter(entry => entry.name.includes('.js'))
57      .forEach(({ name: bundleName, transferSize: size }) => {
58        if (size > this.thresholds.maxBundleSize) {
59          violations.push({
60            type: 'SIZE_VIOLATION',
61            bundle: bundleName,
62            current: size,
63            threshold: this.thresholds.maxBundleSize,
64            severity: 'HIGH'
65          });
66        }
67      });
68
69    return violations;
70  }
71
72  generateRecommendations() {
73    const recommendations = [];
74
75    // Analyse des doublons dans les bundles
76    const duplicatedModules = this.findDuplicatedModules();
77    if (duplicatedModules.length > 0) {
78      recommendations.push({
79        type: 'REMOVE_DUPLICATES',
80        modules: duplicatedModules,
81        estimatedSavings: this.calculateDuplicateSavings(duplicatedModules)
82      });
83    }
84
85    // Analyse des dépendances inutilisées
86    const unusedDeps = this.findUnusedDependencies();
87    if (unusedDeps.length > 0) {
88      recommendations.push({
89        type: 'REMOVE_UNUSED_DEPS',
90        dependencies: unusedDeps,
91        estimatedSavings: this.calculateUnusedDepsSavings(unusedDeps)
92      });
93    }
94
95    return recommendations;
96  }
97}
98
99// Monitoring automatique en production
100if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
101  const monitor = new BundlePerformanceMonitor();
102
103  // Mesure après le chargement de la page
104  window.addEventListener('load', () => {
105    setTimeout(() => {
106      monitor.measureBundlePerformance();
107    }, 2000);
108  });
109}

performance.memory n'existe que dans Chromium, d'où la vérification avant de l'utiliser. Les méthodes auxiliaires (sendToMonitoring, findDuplicatedModules, findUnusedDependencies et le calcul des économies), tu les écris toi-même.

Real User Monitoring (RUM)

PerformanceObserver signale chaque fichier téléchargé ainsi que les tâches longues (long tasks) :

1// RUM pour les performances du bundle
2class RealUserBundleMonitoring {
3  constructor() {
4    this.sessionId = this.generateSessionId();
5    this.startTime = performance.now();
6    this.setupObservers();
7  }
8
9  setupObservers() {
10    // Performance Observer pour le resource timing
11    if ('PerformanceObserver' in window) {
12      const observer = new PerformanceObserver((list) => {
13        list.getEntries().forEach(entry => {
14          if (entry.name.includes('.js') || entry.name.includes('.css')) {
15            this.trackResourceLoad(entry);
16          }
17        });
18      });
19
20      observer.observe({ entryTypes: ['resource'] });
21    }
22
23    // Long task observer
24    if ('PerformanceObserver' in window) {
25      const longTaskObserver = new PerformanceObserver((list) => {
26        list.getEntries().forEach(entry => {
27          this.trackLongTask(entry);
28        });
29      });
30
31      longTaskObserver.observe({ entryTypes: ['longtask'] });
32    }
33  }
34
35  trackResourceLoad(entry) {
36    const resourceData = {
37      sessionId: this.sessionId,
38      timestamp: Date.now(),
39      resourceType: entry.name.includes('.js') ? 'javascript' : 'css',
40      resourceName: entry.name,
41      loadTime: entry.responseEnd - entry.fetchStart,
42      transferSize: entry.transferSize,
43      cached: entry.transferSize === 0,
44      userAgent: navigator.userAgent,
45      connectionType: navigator.connection?.effectiveType
46    };
47
48    this.sendMetric('resource_load', resourceData);
49  }
50
51  trackLongTask(entry) {
52    const longTaskData = {
53      sessionId: this.sessionId,
54      timestamp: Date.now(),
55      duration: entry.duration,
56      startTime: entry.startTime,
57      attribution: entry.attribution
58    };
59
60    this.sendMetric('long_task', longTaskData);
61  }
62
63  async sendMetric(type, data) {
64    // Envoi vers le système d'analytics
65    if (navigator.sendBeacon) {
66      navigator.sendBeacon('/api/metrics', JSON.stringify({
67        type,
68        data,
69        metadata: {
70          url: window.location.href,
71          timestamp: Date.now(),
72          sessionId: this.sessionId
73        }
74      }));
75    }
76  }
77}

Une long task bloque le thread principal pendant au moins 50 ms, et sendBeacon fonctionne même à la fermeture de l'onglet.

Optimiser des bibliothèques précises

Optimiser Lodash

Le tree shaking refait surface ici, c'est-à-dire la suppression automatique du code inutilisé (dead code) dans le bundle final. Il fonctionne sur les modules ES, car import et export sont statiques, contrairement au require de CommonJS. Le champ "sideEffects": false dans package.json indique au bundler que les modules du paquet n'ont pas d'effets de bord, il peut donc écarter ceux qui ne servent pas. Le Lodash classique est écrit en CommonJS :

1# Build d'une version modulaire de Lodash en modules ES (c'est ainsi qu'est généré lodash-es)
2npx lodash-cli modularize exports=es
3
4# Plugin Babel pour une optimisation automatique
5npm install --save-dev babel-plugin-lodash

La deuxième partie est un plugin Babel qui remplace l'import de toute la bibliothèque par des imports de fonctions individuelles :

1// babel.config.js
2module.exports = {
3  plugins: [
4    'lodash' // Remplace automatiquement les imports par des imports cherry-picked
5  ]
6};
7
8// Avant l'optimisation (70KB)
9import _ from 'lodash';
10
11// Après l'optimisation (2KB)
12import debounce from 'lodash/debounce';
13import throttle from 'lodash/throttle';

Aujourd'hui, il est plus simple de passer à lodash-es. Pour la même raison, le pattern Module avec IIFE, même s'il cache les variables privées, ne peut pas être élagué, car il forme un seul objet.

Optimiser les bibliothèques de dates

Moment.js ne prend pas en charge le tree shaking, et ses auteurs recommandent eux-mêmes des alternatives plus récentes :

1// Comparaison de la taille des bibliothèques de dates (indicative, selon la version ; les chiffres actuels sont sur bundlephobia, par exemple)
2const dateLibraryComparison = {
3  'moment': {
4    size: '230KB',
5    gzipped: '67KB',
6    treeshaking: false
7  },
8  'date-fns': {
9    size: '78KB',
10    gzipped: '13KB',
11    treeshaking: true
12  },
13  'dayjs': {
14    size: '9KB',
15    gzipped: '3KB',
16    treeshaking: true
17  },
18  'luxon': {
19    size: '67KB',
20    gzipped: '19KB',
21    treeshaking: true
22  }
23};
24
25// Guide de migration de Moment.js vers date-fns
26const migrationExamples = {
27  before: `
28    import moment from 'moment';
29
30    const formatted = moment().format('DD/MM/YYYY');
31    const isAfter = moment(date1).isAfter(date2);
32  `,
33  after: `
34    import { format, isAfter } from 'date-fns';
35    import { pl } from 'date-fns/locale';
36
37    const formatted = format(new Date(), 'dd/MM/yyyy', { locale: pl });
38    const isAfterResult = isAfter(date1, date2);
39  `
40};

Les tailles sont indicatives. Attention aux tokens : date-fns utilise dd et yyyy, alors que Moment utilise DD et YYYY.

Optimiser les frameworks CSS

PurgeCSS parcourt les fichiers sources et supprime les sélecteurs CSS qu'il n'y a pas trouvés :

1// PurgeCSS pour supprimer les styles inutilisés
2const purgecss = require('@fullhuman/postcss-purgecss');
3
4module.exports = {
5  plugins: [
6    purgecss({
7      content: [
8        './src/**/*.html',
9        './src/**/*.js',
10        './src/**/*.tsx'
11      ],
12      safelist: [
13        /^tooltip-/,
14        /^modal-/,
15        /^dropdown-/
16      ],
17      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
18    })
19  ]
20};
21
22// Exemples de réduction de taille (valeurs illustratives, qui dépendent du projet)
23const cssOptimizationResults = {
24  bootstrap: {
25    before: '150KB',
26    after: '23KB',
27    reduction: '85%'
28  },
29  tailwind: {
30    before: '3.2MB',
31    after: '12KB',
32    reduction: '99.6%'
33  },
34  materialUI: {
35    before: '890KB',
36    after: '145KB',
37    reduction: '84%'
38  }
39};

safelist protège les classes ajoutées dynamiquement par JavaScript.

Optimisation avancée de webpack

Plugins webpack personnalisés

Un plugin maison est une classe dotée d'une méthode apply, qui se branche sur les hooks du compilateur :

1// Plugin d'analyse et d'optimisation du bundle
2class BundleOptimizationPlugin {
3  constructor(options = {}) {
4    this.options = {
5      reportPath: 'bundle-optimization-report.json',
6      thresholds: {
7        maxChunkSize: 250000,
8        maxModuleSize: 50000
9      },
10      ...options
11    };
12  }
13
14  apply(compiler) {
15    compiler.hooks.emit.tapAsync('BundleOptimizationPlugin', (compilation, callback) => {
16      const report = this.generateOptimizationReport(compilation);
17
18      // Enregistrer le rapport
19      compilation.assets[this.options.reportPath] = {
20        source: () => JSON.stringify(report, null, 2),
21        size: () => JSON.stringify(report, null, 2).length
22      };
23
24      // Journaliser les avertissements
25      this.logWarnings(compilation, report);
26
27      callback();
28    });
29  }
30
31  generateOptimizationReport(compilation) {
32    const chunks = Array.from(compilation.chunks);
33    const modules = Array.from(compilation.modules);
34
35    return {
36      timestamp: new Date().toISOString(),
37      summary: {
38        totalChunks: chunks.length,
39        totalModules: modules.length,
40        totalSize: this.calculateTotalSize(chunks)
41      },
42      chunks: chunks.map(chunk => ({
43        id: chunk.id,
44        name: chunk.name,
45        size: chunk.size,
46        modules: chunk.getModules().length,
47        parents: Array.from(chunk.groupsIterable, g => g.getParents()),
48        children: Array.from(chunk.groupsIterable, g => g.getChildren())
49      })),
50      optimizationOpportunities: this.findOptimizationOpportunities(chunks, modules),
51      recommendations: this.generateRecommendations(chunks, modules)
52    };
53  }
54
55  findOptimizationOpportunities(chunks, modules) {
56    const opportunities = [];
57
58    // Trouver les gros modules
59    const largeModules = modules.filter(module =>
60      module.size() > this.options.thresholds.maxModuleSize
61    );
62
63    if (largeModules.length > 0) {
64      opportunities.push({
65        type: 'LARGE_MODULES',
66        modules: largeModules.map(m => ({
67          identifier: m.identifier(),
68          size: m.size(),
69          reasons: m.reasons.map(r => r.module?.identifier())
70        }))
71      });
72    }
73
74    // Trouver les doublons
75    const moduleDuplicates = this.findModuleDuplicates(modules);
76    if (moduleDuplicates.length > 0) {
77      opportunities.push({
78        type: 'MODULE_DUPLICATES',
79        duplicates: moduleDuplicates
80      });
81    }
82
83    return opportunities;
84  }
85}
86
87// Utilisation du plugin
88module.exports = {
89  plugins: [
90    new BundleOptimizationPlugin({
91      thresholds: {
92        maxChunkSize: 200000,
93        maxModuleSize: 30000
94      }
95    })
96  ]
97};

C'est une ébauche dans l'API de webpack 4, et les méthodes auxiliaires (calculateTotalSize, findModuleDuplicates, generateRecommendations, logWarnings), tu les écris toi-même. Dans webpack 5, tu ajoutes les ressources dans le hook processAssets avec compilation.emitAsset, tu obtiens la taille d'un chunk avec compilation.chunkGraph.getChunkSize(chunk) et tu remplaces module.reasons, qui n'existe plus, par compilation.moduleGraph.getIncomingConnections(module).

Optimiser le runtime chunk

Le runtime est le code de webpack qui charge les chunks, et une fois extrait, il n'invalide plus le cache de l'application. À côté, on trouve la minification du JS et du CSS :

1// Optimisation du runtime chunk
2const TerserPlugin = require('terser-webpack-plugin');
3const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
4
5module.exports = {
6  optimization: {
7    runtimeChunk: {
8      name: entrypoint => `runtime-${entrypoint.name}`
9    },
10
11    // Minimizer configuration
12    minimizer: [
13      new TerserPlugin({
14        parallel: true,
15        terserOptions: {
16          parse: {
17            ecma: 2020
18          },
19          compress: {
20            ecma: 2020,
21            comparisons: false,
22            inline: 2,
23            drop_console: process.env.NODE_ENV === 'production',
24            drop_debugger: process.env.NODE_ENV === 'production',
25            pure_funcs: ['console.log', 'console.info', 'console.debug']
26          },
27          mangle: {
28            safari10: true
29          },
30          output: {
31            ecma: 2020,
32            comments: false,
33            ascii_only: true
34          }
35        }
36      }),
37
38      new CssMinimizerPlugin({
39        parallel: true,
40        minimizerOptions: {
41          preset: [
42            'default',
43            {
44              discardComments: { removeAll: true },
45              normalizeWhitespace: true,
46              colormin: true,
47              convertValues: true,
48              discardDuplicates: true,
49              discardEmpty: true,
50              mergeRules: true,
51              minifyFontValues: true,
52              minifyGradients: true,
53              minifyParams: true,
54              minifySelectors: true,
55              normalizeCharset: true,
56              normalizeDisplayValues: true,
57              normalizePositions: true,
58              normalizeRepeatStyle: true,
59              normalizeString: true,
60              normalizeTimingFunctions: true,
61              normalizeUnicode: true,
62              normalizeUrl: true,
63              orderedValues: true,
64              reduceIdents: true,
65              reduceInitial: true,
66              reduceTransforms: true,
67              svgo: true,
68              uniqueSelectors: true
69            }
70          ]
71        }
72      })
73    ]
74  }
75};

drop_console n'agit qu'en production, et pure_funcs désigne les fonctions dont les appels peuvent être supprimés.

Tester le bundle

Tests automatisés du bundle

Un seul import suffit à casser une optimisation, c'est pourquoi des tests surveillent la taille, les doublons et le tree shaking :

1// Suite de tests pour l'optimisation du bundle
2const bundleTests = {
3  // Test de la taille du bundle
4  async testBundleSize() {
5    const bundleStats = await getBundleStats();
6    const maxSize = 500000; // 500KB
7
8    expect(bundleStats.totalSize).toBeLessThan(maxSize);
9    expect(bundleStats.mainChunk).toBeLessThan(200000);
10    expect(bundleStats.vendorChunk).toBeLessThan(300000);
11  },
12
13  // Test des doublons
14  async testNoDuplicateModules() {
15    const bundleAnalysis = await analyzeBundleContent();
16    const duplicates = bundleAnalysis.duplicatedModules;
17
18    expect(duplicates).toHaveLength(0);
19  },
20
21  // Test de l'efficacité du tree shaking
22  async testTreeShakingEffectiveness() {
23    const unusedExports = await findUnusedExports();
24    const deadCode = await findDeadCode();
25
26    expect(unusedExports.length).toBeLessThan(5);
27    expect(deadCode.totalSize).toBeLessThan(10000);
28  },
29
30  // Test de l'efficacité du cache
31  async testCacheability() {
32    const chunks = await getChunkManifest();
33
34    chunks.forEach(chunk => {
35      if (chunk.type === 'vendor') {
36        expect(chunk.hash).toMatch(/^[a-f0-9]{8}$/);
37        expect(chunk.cacheable).toBe(true);
38      }
39    });
40  }
41};
42
43// Intégration à la CI/CD
44describe('Bundle Optimization Tests', () => {
45  test('Bundle size within limits', bundleTests.testBundleSize);
46  test('No duplicate modules', bundleTests.testNoDuplicateModules);
47  test('Tree shaking effective', bundleTests.testTreeShakingEffectiveness);
48  test('Chunks properly cacheable', bundleTests.testCacheability);
49});

Les fonctions auxiliaires, comme getBundleStats, tu les écris toi-même à partir de stats.json.

Détecter les régressions de performance

Le détecteur de régressions compare les métriques actuelles à un point de référence enregistré (baseline) :

1// Système de détection des régressions de performance
2class PerformanceRegressionDetector {
3  constructor() {
4    this.baselineMetrics = null;
5    this.threshold = 0.1; // hausse de 10 % = régression
6  }
7
8  async setBaseline() {
9    this.baselineMetrics = await this.measureCurrentMetrics();
10    await this.saveBaseline(this.baselineMetrics);
11  }
12
13  async detectRegressions() {
14    const currentMetrics = await this.measureCurrentMetrics();
15    const regressions = [];
16
17    if (!this.baselineMetrics) {
18      this.baselineMetrics = await this.loadBaseline();
19    }
20
21    // Vérifier la taille des bundles
22    const sizeRegression = this.detectSizeRegression(
23      this.baselineMetrics.bundleSize,
24      currentMetrics.bundleSize
25    );
26
27    if (sizeRegression) {
28      regressions.push(sizeRegression);
29    }
30
31    // Vérifier les temps de chargement
32    const loadTimeRegression = this.detectLoadTimeRegression(
33      this.baselineMetrics.loadTime,
34      currentMetrics.loadTime
35    );
36
37    if (loadTimeRegression) {
38      regressions.push(loadTimeRegression);
39    }
40
41    return regressions;
42  }
43
44  detectSizeRegression(baseline, current) {
45    const increase = (current - baseline) / baseline;
46
47    if (increase > this.threshold) {
48      return {
49        type: 'SIZE_REGRESSION',
50        baseline: baseline,
51        current: current,
52        increase: `${(increase * 100).toFixed(1)}%`,
53        severity: increase > 0.25 ? 'HIGH' : 'MEDIUM'
54      };
55    }
56
57    return null;
58  }
59
60  async generateRegressionReport(regressions) {
61    return {
62      timestamp: new Date().toISOString(),
63      regressions: regressions,
64      recommendations: await this.generateRecommendations(regressions),
65      actionItems: this.generateActionItems(regressions)
66    };
67  }
68}

Une hausse de plus de 10 % signale une régression, et au-delà de 25 %, une régression de haute priorité.

Stratégies de déploiement en production

Tu peux déployer un nouveau bundle progressivement et comparer des variantes sur du trafic réel :

1// Stratégie de progressive deployment pour les bundles
2class ProgressiveBundleDeployment {
3  constructor() {
4    this.deploymentStages = [
5      { name: 'canary', percentage: 5 },
6      { name: 'blue-green', percentage: 50 },
7      { name: 'full', percentage: 100 }
8    ];
9  }
10
11  async deployNewBundle(bundleVersion) {
12    for (const stage of this.deploymentStages) {
13      console.log(`Deploying to ${stage.name} (${stage.percentage}% traffic)`);
14
15      await this.deployToStage(bundleVersion, stage);
16
17      const metrics = await this.monitorStageMetrics(stage, 300000); // 5 min
18
19      if (!this.validateStageSuccess(metrics)) {
20        await this.rollbackStage(stage);
21        throw new Error(`Deployment failed at ${stage.name} stage`);
22      }
23
24      console.log(`Stage ${stage.name} successful`);
25    }
26  }
27
28  async monitorStageMetrics(stage, duration) {
29    const startTime = Date.now();
30    const metrics = {
31      errorRate: [],
32      loadTimes: [],
33      bundleErrors: []
34    };
35
36    while (Date.now() - startTime < duration) {
37      const sample = await this.collectMetricsSample(stage);
38      metrics.errorRate.push(sample.errorRate);
39      metrics.loadTimes.push(sample.avgLoadTime);
40      metrics.bundleErrors.push(sample.bundleErrors);
41
42      await new Promise(resolve => setTimeout(resolve, 30000)); // 30s intervals
43    }
44
45    return metrics;
46  }
47
48  validateStageSuccess(metrics) {
49    const avgErrorRate = metrics.errorRate.reduce((a, b) => a + b) / metrics.errorRate.length;
50    const avgLoadTime = metrics.loadTimes.reduce((a, b) => a + b) / metrics.loadTimes.length;
51    const totalBundleErrors = metrics.bundleErrors.reduce((a, b) => a + b);
52
53    return avgErrorRate < 0.01 && // < 1% error rate
54           avgLoadTime < 3000 &&  // < 3s load time
55           totalBundleErrors < 5; // < 5 bundle errors
56  }
57}
58
59// A/B testing de différentes stratégies d'optimisation du bundle
60class BundleOptimizationABTest {
61  constructor() {
62    this.variants = {
63      control: {
64        strategy: 'current',
65        splitChunks: 'default',
66        compression: 'gzip'
67      },
68      optimized: {
69        strategy: 'aggressive_splitting',
70        splitChunks: 'custom',
71        compression: 'brotli'
72      },
73      experimental: {
74        strategy: 'module_federation',
75        splitChunks: 'micro_chunks',
76        compression: 'zstd'
77      }
78    };
79  }
80
81  async runABTest(duration = 7 * 24 * 60 * 60 * 1000) { // 7 days
82    const testId = this.generateTestId();
83
84    // Lancer le test
85    await this.startTest(testId);
86
87    // Surveiller les métriques
88    const results = await this.monitorTest(testId, duration);
89
90    // Analyser les résultats
91    const analysis = await this.analyzeResults(results);
92
93    // Choisir le gagnant
94    const winner = this.selectWinner(analysis);
95
96    return {
97      testId,
98      duration,
99      results,
100      analysis,
101      winner,
102      recommendation: this.generateRecommendation(analysis, winner)
103    };
104  }
105
106  async monitorTest(testId, duration) {
107    const results = {
108      control: { metrics: [], samples: 0 },
109      optimized: { metrics: [], samples: 0 },
110      experimental: { metrics: [], samples: 0 }
111    };
112
113    const startTime = Date.now();
114
115    while (Date.now() - startTime < duration) {
116      for (const variant of Object.keys(this.variants)) {
117        const metrics = await this.collectVariantMetrics(variant);
118        results[variant].metrics.push(metrics);
119        results[variant].samples++;
120      }
121
122      await new Promise(resolve => setTimeout(resolve, 3600000)); // 1 hour
123    }
124
125    return results;
126  }
127
128  selectWinner(analysis) {
129    const scores = {};
130
131    for (const variant of Object.keys(analysis)) {
132      const data = analysis[variant];
133
134      scores[variant] = (
135        (1 / data.avgLoadTime) * 0.4 +     // 40% weight
136        (1 / data.avgBundleSize) * 0.3 +   // 30% weight
137        data.conversionRate * 0.2 +        // 20% weight
138        (1 - data.errorRate) * 0.1         // 10% weight
139      );
140    }
141
142    return Object.keys(scores).reduce((a, b) =>
143      scores[a] > scores[b] ? a : b
144    );
145  }
146}

L'étape blue-green est une simplification : un blue-green classique bascule tout le trafic d'un coup, alors que l'augmentation progressive du trafic correspond à une canary release.

Récapitulatif et checklist

Pour finir, une checklist et un calcul hypothétique du retour sur investissement :

1const bundleOptimizationChecklist = {
2  analysis: [
3    'Webpack Bundle Analyzer setup',
4    'Source map analysis',
5    'Performance budget configured',
6    'CI/CD bundle size monitoring',
7    'Duplicate detection'
8  ],
9
10  codeOptimization: [
11    'Tree shaking enabled',
12    'Dead code elimination',
13    'Unused dependencies removed',
14    'Code splitting implemented',
15    'Lazy loading configured'
16  ],
17
18  bundleStrategy: [
19    'Chunk splitting optimized',
20    'Vendor chunks separated',
21    'Runtime chunk extracted',
22    'Common chunks identified',
23    'Dynamic imports utilized'
24  ],
25
26  compression: [
27    'Gzip/Brotli enabled',
28    'Minification configured',
29    'CSS optimization',
30    'Image optimization',
31    'Font optimization'
32  ],
33
34  caching: [
35    'Long-term caching setup',
36    'Content-based hashing',
37    'Cache invalidation strategy',
38    'Service worker implemented',
39    'CDN optimization'
40  ],
41
42  monitoring: [
43    'Real user monitoring',
44    'Performance metrics tracking',
45    'Regression detection',
46    'A/B testing capability',
47    'Alert system configured'
48  ]
49};
50
51// ROI hypothétique (valeurs illustratives, pas des données d'études)
52const optimizationROI = {
53  investment: {
54    developmentTime: '2-3 weeks',
55    toolingSetup: '$5,000',
56    ongoingMaintenance: '$2,000/month'
57  },
58
59  returns: {
60    conversionRateIncrease: '+25-40%',
61    bounceRateDecrease: '-30-50%',
62    serverCostReduction: '-20-30%',
63    mobileUserRetention: '+35-60%',
64    seoRankingImprovement: '+15-25%'
65  },
66
67  businessImpact: {
68    revenueIncrease: '$50,000-150,000/year',
69    costSavings: '$20,000-40,000/year',
70    paybackPeriod: '2-4 months',
71    totalROI: '400-800%'
72  }
73};

L'optimisation est généralement rentable, mais le retour dépend du projet, alors appuie-toi sur tes propres mesures. Commence par Bundle Analyzer car une seule carte des modules désigne généralement le plus gros coupable. Dans l'éditeur ci-dessous, tu construiras le tableau de bord du parc, et dans la leçon suivante, tu découvriras l'architecture orientée événements.

Retiens : chaque kilo-octet du bundle est une charge que les visiteurs doivent faire passer par la porte d'entrée, alors n'emporte que l'indispensable.

Code de cette leçon : index.js
1// Tableau de bord et interface du parc
2// Exercice : implémente un système d'affichage du tableau de bord
3
4class ParkDashboard {
5  constructor() {
6    this.widgets = [];
7    this.data = {};
8  }
9
10  // Implémente les méthodes :
11  // addWidget(name, renderFn) - ajoute un widget
12  // updateData(key, value) - met à jour les données
13  // render() - affiche tous les widgets
14
15  addWidget(name, renderFn) {
16    // Ton code ici
17  }
18
19  updateData(key, value) {
20    // Ton code ici
21  }
22
23  render() {
24    // Ton code ici
25  }
26}
27
28const dashboard = new ParkDashboard();
29console.log("Tableau de bord prêt à être configuré");

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. Qu'est-ce que le tree shaking dans le contexte du bundling JavaScript ?

  2. 2. Quel format de modules permet un tree shaking efficace ?

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

Tâches pratiques dans le jeu

  • Tri vertical

    Range les étapes du refactoring d'un code vers un pattern de conception :

  • Éditeur de code

    Le fichier index.js contient trois tests unitaires : des fonctions qui reçoivent la classe à vérifier et doivent retourner true pour une classe correcte et false pour une classe avec un bug. Complète les blancs : ___BLANK1___ est l'opérateur de comparaison qui vérifie que getModule retourne le même objet que celui enregistré, ___BLANK2___ est l'opérateur de comparaison qui vérifie que deux id sont différents, et ___BLANK3___ est le nombre d'alertes attendu quand un seul des deux capteurs dépasse son seuil. Le correcteur lance tes tests sur des classes ParkCore, DinosaurRegistry et SecuritySystem correctes et sur des versions avec des bugs (une copie du module au lieu de l'original, un seul id pour tous, une alarme pour une mesure égale au seuil).

  • Tri vertical

    Range les éléments de l'interface dans l'ordre où ils apparaissent dans le code HTML de la page (en commençant par le conteneur extérieur) :

  • Éditeur de code

    Le fichier index.js contient une classe PerformanceMonitor. Complète les blancs : ___BLANK1___ est la méthode de l'objet performance qui retourne l'heure actuelle en millisecondes avec une haute précision (measure utilise la même méthode pour l'heure de fin), ___BLANK2___ est la méthode de tableau qui additionne toutes les durées (elle part de 0), et ___BLANK3___ est la méthode de Math qui retourne la plus grande durée. measure(name, fn) doit retourner le résultat de fn et enregistrer la mesure { name, duration }, et getReport(name) doit retourner { count, min, max, avg } pour les mesures portant ce nom, ou null quand il n'y en a pas.

  • Clique dans l'ordre

    Range les étapes de mise en place du lazy loading d'un composant en JavaScript :

  • Tri horizontal

    Range les éléments d'un export de module dans le bon ordre :

  • Clique dans l'ordre

    Range les éléments de l'Observer Pattern :

  • Tri vertical

    Range les étapes d'implémentation du Factory Pattern dans le bon ordre :

  • Tri vertical

    Range le flux de données dans le système de gestion des dinosaures :

  • Tri vertical

    Range les éléments d'une injection de dépendances dans le bon ordre :

  • Tri vertical

    Range les étapes de mise en place du code splitting dans une application :

  • Tri vertical

    Range les étapes d'implémentation de l'Observer Pattern dans une application :

  • Tri horizontal

    Range les éléments du gel d'un objet :

Articles utiles