Retour au cours

frontend / javascript

Outils modernes : bundlers et ESM

Leçon 231 exercice

Explication

Ce que vous allez apprendre

  • Décrire les rôles successifs d'un bundler (regroupement, transpilation, minification)
  • Expliquer pourquoi Babel transpile une syntaxe moderne vers une syntaxe plus ancienne
  • Relier le tree-shaking à l'analyse statique des imports ESM vue à la leçon 14
  • Utiliser import() dynamique pour découper une application en morceaux chargés à la demande
  • Diagnostiquer un temps de chargement initial trop long lié à un bundle non découpé

Dans quel contexte ?

Une application React met 8 secondes à afficher sa première page, même sur une bonne connexion. En inspectant l'onglet Réseau, un développeur découvre un unique fichier bundle.js de 4 Mo, qui contient tout le code de l'application, y compris des pages d'administration que 99% des visiteurs ne verront jamais. En remplaçant les imports statiques de ces pages par des import() dynamiques, chargés seulement quand l'utilisateur navigue réellement vers ces routes, le bundle initial passe à 400 Ko et le temps de premier affichage chute drastiquement.

1. Le code que vous écrivez n'est pas celui qui est livré

Le code JavaScript que vous écrivez n'est presque jamais celui qui arrive tel quel dans le navigateur de vos utilisateurs. Entre les deux s'intercale une chaîne d'outils, les bundlers, qui remplit plusieurs rôles essentiels avant la mise en production.

2. Premier rôle : regrouper les fichiers

Un projet réel est composé de dizaines, voire de centaines de fichiers modules (leçon 14). Les regrouper en un nombre réduit de fichiers évite au navigateur de devoir effectuer autant de requêtes réseau séparées.

Prérequis

Cette leçon s'appuie directement sur la distinction CommonJS/modules ES vue à la leçon 14 : le tree-shaking décrit plus bas n'est possible qu'avec la syntaxe import/export analysable statiquement.

3. Deuxième rôle : rendre le code compatible

Tous les navigateurs ciblés ne supportent pas forcément les toutes dernières fonctionnalités du langage. Un outil comme Babel transpile la syntaxe moderne vers une version plus ancienne et largement compatible.

4. Troisième rôle : réduire la taille du fichier

Le code est ensuite minifié — noms de variables raccourcis, espaces supprimés — pour réduire la taille du fichier téléchargé par chaque visiteur.

Étape du bundlerRôleOutil typique
BundlingRegrouper les modules en peu de fichiersWebpack, Vite, esbuild
TranspilationRendre la syntaxe compatible avec les navigateurs ciblésBabel
MinificationRéduire la taille du fichier livréTerser
Tree-shakingÉliminer le code jamais utiliséIntégré aux bundlers modernes

5. Le tree-shaking, rendu possible par ESM

Parce que les imports/exports ESM sont analysables statiquement, le bundler peut déterminer avec certitude quel code est réellement utilisé et éliminer le reste avant de le livrer au navigateur.

Astuce

Pour bénéficier pleinement du tree-shaking, importez uniquement ce dont vous avez besoin (import { debounce } from "lodash-es") plutôt que la bibliothèque entière (import _ from "lodash"), et préférez toujours une bibliothèque distribuée en modules ES quand elle existe.

6. Charger seulement ce qui est nécessaire

Enfin, le code-splitting, via import() dynamique, permet de ne charger certaines parties de l'application qu'au moment où l'utilisateur en a réellement besoin, plutôt que de tout télécharger dès le premier chargement.

Commandes & code

Outils modernes : bundlers et écosystème

Comment le JavaScript moderne est transformé avant d'arriver en production.

js
// --- Pourquoi un bundler ? ---
// 1. Regrouper des dizaines/centaines de modules en peu de fichiers (moins de requetes HTTP)
// 2. Transpiler la syntaxe moderne vers une syntaxe compatible avec les navigateurs cibles
// 3. Minifier (reduire la taille) et faire du tree-shaking (eliminer le code mort)
// 4. Gerer les assets (CSS, images, fonts) comme des dependances importables

// fichier source : index.js
import { formaterDate } from "./utils/dates.js";
import "./styles/main.css";               // les bundlers modernes gerent aussi le CSS
import logo from "./assets/logo.png";     // les images deviennent des URLs/imports

console.log(formaterDate(new Date()));
js
// --- esbuild : bundler/transpileur extremement rapide (ecrit en Go) ---
// esbuild.config.js
require("esbuild").build({
    entryPoints: ["src/index.js"],
    bundle: true,
    minify: true,
    sourcemap: true,
    outfile: "dist/bundle.js",
    target: ["es2020"],
});
js
// --- Vite : dev server ultra-rapide (ESM natif en dev) + Rollup pour le build de prod ---
// vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        target: "es2020",
        sourcemap: true,
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ["react", "react-dom"],    // code-splitting manuel
                },
            },
        },
    },
    server: {
        port: 3000,
    },
});
json
// --- Babel : transpilation de syntaxe moderne vers une syntaxe compatible anciens navigateurs ---
{
    "presets": [
        ["@babel/preset-env", { "targets": "> 0.25%, not dead" }]
    ]
}
js
// Exemple : Babel transforme les optional chaining/nullish coalescing modernes
// en equivalent compatible avec d'anciens moteurs JS

// Code source (moderne)
const ville = utilisateur?.adresse?.ville ?? "inconnue";

// Sortie transpilee (simplifiee, compatible ES5)
// var ville = (utilisateur != null && utilisateur.adresse != null)
//     ? (utilisateur.adresse.ville != null ? utilisateur.adresse.ville : "inconnue")
//     : "inconnue";

// --- Code-splitting : charger le JS uniquement quand necessaire ---
async function chargerGraphiques() {
    // le bundler cree un chunk SEPARE pour ce module, charge a la demande seulement
    const { tracerGraphique } = await import("./graphiques.js");
    tracerGraphique(document.querySelector("#canvas"));
}
document.querySelector("#bouton-stats")?.addEventListener("click", chargerGraphiques);

// --- Tree-shaking : eliminer le code jamais utilise ---
// fichier utils.js exporte 50 fonctions, mais seule "formaterDate" est importee ailleurs
// export function formaterDate(d) { ... }
// export function fonctionJamaisUtilisee() { ... }    // eliminee du bundle final par le bundler

// --- Source maps : deboguer le code transforme comme s'il etait source ---
// Un fichier .map associe chaque ligne du bundle minifie a sa position dans le fichier original,
// permettant au navigateur d'afficher le code source lisible dans les DevTools.
bash
# --- npm scripts : orchestrer les taches de build ---
npm run build          # execute le script "build" defini dans package.json
npm run dev              # lance le serveur de developpement
json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "jest",
    "lint": "eslint src/"
  }
}

Résumé

  • Un bundler regroupe, transpile, minifie et fait du tree-shaking sur les modules d'un projet.
  • Vite utilise ESM natif en développement (rapide) et Rollup pour un build de production optimisé.
  • Babel transpile la syntaxe moderne vers une cible compatible avec les navigateurs visés.
  • Le code-splitting (import() dynamique) réduit le JS chargé initialement, amélioration directe du temps de chargement.

Exercices pratiques

1 disponible
1

Mission : dégonfler un bundle de 3.8 Mo

Objectif : Diagnostiquer pourquoi un module admin et une librairie entière finissent dans le bundle initial, puis corriger avec import() dynamique et un import nommé ESM.

Contexte

Le fichier main.js d'une application de gestion contient ceci :

js
import _ from "lodash";
import { PanneauAdmin } from "./admin.js";

document.querySelector("#recherche").addEventListener("input", _.debounce(lancerRecherche, 300));

if (utilisateurEstAdmin()) {
    PanneauAdmin.afficher();
}

Le bundle de production pèse 3.8 Mo alors que 95% des visiteurs ne sont jamais admin, et que _.debounce est la seule fonction de lodash réellement utilisée dans tout le projet.

Résoudre l’exercice →