Optimisation du Bundle JavaScript avec Vite en 2026 : Code Splitting, Tree Shaking et Budget de Performance

Réduisez votre bundle JS avec Vite en 2026. Diagnostic avec rollup-plugin-visualizer, code splitting, tree shaking, remplacement des dépendances lourdes, compression Brotli et budget de performance CI avec du code prêt à l'emploi.

Introduction : Votre bundle JavaScript est probablement trop gros

Voici un chiffre qui devrait vous faire tiquer : en 2024, le payload JavaScript médian d'une page web atteint 558 Ko sur mobile et 613 Ko sur desktop, selon le HTTP Archive. C'est 14 % de plus qu'un an plus tôt. Et franchement, la tendance ne montre aucun signe de ralentissement — en 2010, on était à 90 Ko. On a multiplié le poids par sept en quatorze ans.

Le truc, c'est que le JavaScript n'est pas une image. Une image, le navigateur la décode et l'affiche, point. Le JavaScript, lui, doit être téléchargé, parsé, compilé et exécuté — et chacune de ces étapes bloque le thread principal.

Résultat : pendant que votre bundle de 800 Ko se charge, l'utilisateur fixe un écran blanc. Les clics ne répondent pas. L'INP explose. Et 53 % de vos visiteurs mobiles sont déjà partis avant même d'avoir vu quoi que ce soit.

Si vous avez lu notre guide sur l'optimisation de l'INP, vous savez déjà que les longues tâches JavaScript sont l'ennemi numéro un de la réactivité. Mais avant même de découper vos tâches runtime avec scheduler.yield(), il y a une question plus fondamentale : pourquoi envoyez-vous autant de JavaScript au départ ?

C'est exactement ce qu'on va voir dans ce guide. On va couvrir toute la chaîne d'optimisation du bundle : analyse avec rollup-plugin-visualizer, code splitting automatique et manuel avec Vite, tree shaking efficace, remplacement des dépendances lourdes, et mise en place d'un budget de performance. Chaque section contient du code fonctionnel, prêt à copier-coller dans votre projet.

Pourquoi Vite est devenu le standard en 2026

Avant de plonger dans les techniques, un mot rapide sur l'outil. En 2026, Vite s'est imposé comme le bundler de référence pour le développement web. Les téléchargements npm sont passés de 7,5 millions à 17 millions par semaine en un an. C'est le bundler par défaut pour Vue, Nuxt, Svelte, SvelteKit, Solid, Astro et bien d'autres.

Ce qui rend Vite particulièrement adapté à l'optimisation de bundle, c'est son architecture. En développement, il sert le code source via les modules ES natifs du navigateur — pas de bundling, donc un démarrage quasi instantané. En production, il utilise Rollup (et bientôt Rolldown, son successeur écrit en Rust) pour produire des bundles optimisés avec tree shaking et code splitting automatiques.

Et les chiffres de la version 8 Beta, propulsée par Rolldown, sont honnêtement impressionnants : démarrage du serveur de développement 3x plus rapide, rechargements complets 40 % plus rapides, et jusqu'à 10x moins de requêtes réseau. GitLab rapporte même une réduction de 100x de la consommation mémoire en pic lors des builds. Autant dire que l'investissement dans Vite paie largement.

Étape 1 : Diagnostiquer — Analyser votre bundle

On ne peut pas optimiser ce qu'on ne mesure pas. C'est un peu bateau comme phrase, mais c'est tellement vrai pour les bundles JS. Avant de toucher à quoi que ce soit, il faut visualiser la composition de votre bundle. L'outil de référence pour ça avec Vite, c'est rollup-plugin-visualizer.

Installation et configuration

# Installation
npm install rollup-plugin-visualizer -D

Ensuite, ajoutez le plugin à votre configuration Vite. Un point important : il doit être en dernier dans la liste des plugins, sinon les résultats seront faussés.

// vite.config.ts
import { defineConfig, type PluginOption } from "vite";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    // ... vos autres plugins
    visualizer({
      filename: "stats.html",
      open: true,        // Ouvre automatiquement dans le navigateur
      gzipSize: true,    // Affiche la taille gzip
      brotliSize: true,  // Affiche la taille Brotli
      template: "treemap" // treemap, sunburst, network, flamegraph
    }) as PluginOption,
  ],
});

Lancez un build de production :

npm run build

Un fichier stats.html s'ouvre dans votre navigateur. Vous voyez une carte thermique (treemap) où chaque rectangle représente un module — plus le rectangle est gros, plus le module pèse dans le bundle final. Les modules en vert proviennent de node_modules (vos dépendances), ceux en bleu sont votre code applicatif.

Quoi chercher dans la visualisation

En examinant le treemap, concentrez-vous sur trois choses :

  • Les gros rectangles verts : ce sont vos dépendances les plus lourdes. C'est souvent là que se cachent les gains les plus faciles. Un lodash non tree-shaké de 71 Ko, un moment.js de 290 Ko avec toutes ses locales, un chart.js importé en entier alors que vous n'utilisez qu'un type de graphique... on a tous eu ça au moins une fois.
  • Les modules dupliqués : si la même librairie apparaît dans plusieurs chunks, il y a un problème de configuration.
  • Les chunks déséquilibrés : un chunk principal énorme avec des chunks secondaires minuscules ? Ça indique un code splitting insuffisant.

Vous pouvez aussi utiliser l'outil en ligne de commande sans toucher à votre config :

npx vite-bundle-visualizer

Étape 2 : Code Splitting — Ne chargez que ce qui est nécessaire

Le code splitting, c'est probablement la technique la plus impactante pour réduire le bundle initial. L'idée est simple : au lieu de générer un seul fichier JavaScript monolithique, on le découpe en chunks plus petits qui se chargent à la demande.

Code splitting automatique avec les imports dynamiques

Vite effectue automatiquement le code splitting pour chaque import dynamique. C'est la méthode la plus simple et, honnêtement, souvent la plus efficace :

// ❌ Import statique — tout est inclus dans le bundle principal
import { Dashboard } from "./pages/Dashboard";
import { Settings } from "./pages/Settings";
import { Analytics } from "./pages/Analytics";

// ✅ Import dynamique — chaque page est un chunk séparé
const Dashboard = () => import("./pages/Dashboard");
const Settings = () => import("./pages/Settings");
const Analytics = () => import("./pages/Analytics");

Avec React, combinez ça avec React.lazy() et Suspense :

import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const Analytics = lazy(() => import("./pages/Analytics"));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Chargement...</div>}>
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="/analytics" element={<Analytics />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Chaque route devient un chunk distinct. L'utilisateur qui visite uniquement le Dashboard ne télécharge jamais le code de Settings ou Analytics. En production, cette approche réduit typiquement le bundle initial de 30 à 50 %. Et ça, pour quelques lignes de code changées, c'est un excellent retour sur investissement.

Code splitting manuel avec manualChunks

Pour un contrôle plus fin, Vite expose l'option manualChunks de Rollup. Elle permet de regrouper vos dépendances en chunks nommés et cohérents.

Forme objet — la plus simple :

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          "vendor-react": ["react", "react-dom", "react-router-dom"],
          "vendor-ui": ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu"],
          "vendor-charts": ["recharts", "d3"],
        },
      },
    },
  },
});

Forme fonction — pour ceux qui veulent une granularité maximale :

// vite.config.ts
export default defineConfig({
  build: {
    chunkSizeWarningLimit: 500, // Avertissement au-dessus de 500 Ko
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            const pkg = id.split("node_modules/")[1].split("/")[0];

            // Framework React dans son propre chunk
            if (["react", "react-dom", "react-router-dom"].includes(pkg)) {
              return "vendor-react";
            }
            // Librairies de graphiques
            if (["recharts", "d3", "d3-scale", "d3-shape"].includes(pkg)) {
              return "vendor-charts";
            }
            // Tout le reste des vendors dans un chunk commun
            return "vendor";
          }
        },
      },
    },
  },
});

L'écueil des dépendances croisées

Attention, la forme fonction de manualChunks comporte un piège que j'ai vu piéger pas mal de devs (moi y compris). Si deux modules que vous placez dans des chunks différents dépendent l'un de l'autre, Rollup doit charger les deux dans le bon ordre. Dans le pire des cas, un chunk censé être « lazy » se retrouve chargé de manière synchrone parce qu'un module helper commun y a été placé par erreur.

La règle d'or : les modules co-dépendants doivent être dans le même chunk. Après chaque modification de manualChunks, relancez le visualizer pour vérifier que votre découpage a bien l'effet attendu.

Étape 3 : Tree Shaking — Éliminer le code mort

Le tree shaking, c'est le processus par lequel le bundler analyse les imports/exports de vos modules ES et supprime le code qui n'est jamais utilisé. Vite (via Rollup) le fait automatiquement en production. Mais encore faut-il que votre code soit tree-shakeable — et c'est là que ça se complique souvent.

Règle 1 : Utilisez les modules ES, pas CommonJS

// ✅ Module ES — tree-shakeable
import { debounce } from "lodash-es";

// ❌ CommonJS — tout lodash est inclus (71 Ko)
const _ = require("lodash");

// ❌ Import par défaut — tout lodash est inclus
import _ from "lodash";

La différence est brutale. Avec lodash-es et un import nommé, seule la fonction debounce (environ 1 Ko) est incluse. Avec lodash en CommonJS, ce sont les 71 Ko de la librairie entière qui atterrissent dans votre bundle. Pour une seule fonction.

Règle 2 : Évitez les barrel files

Les barrel files (ces fameux fichiers index.ts qui ré-exportent tout) sont l'ennemi juré du tree shaking. Même si vous n'importez qu'un seul composant, le bundler peut être forcé d'évaluer tout le barrel :

// components/index.ts — barrel file
export { Button } from "./Button";
export { Modal } from "./Modal";
export { DataTable } from "./DataTable";
export { Chart } from "./Chart";

// ❌ Import depuis le barrel — risque d'inclure tout
import { Button } from "./components";

// ✅ Import direct — garanti tree-shakeable
import { Button } from "./components/Button";

C'est un peu plus verbeux, oui. Mais la différence dans le bundle final peut être significative, surtout si vos composants importent eux-mêmes des dépendances lourdes.

Règle 3 : Déclarez les modules sans effets de bord

Le bundler ne peut pas supprimer un module s'il soupçonne que celui-ci a des effets de bord (side effects) — du code qui s'exécute à l'import, comme une modification de variable globale. Déclarez explicitement vos modules comme purs dans package.json :

{
  "name": "mon-app",
  "sideEffects": false
}

Si certains fichiers ont des effets de bord légitimes (comme un fichier CSS importé en JS, ce qui est assez courant), listez-les explicitement :

{
  "sideEffects": ["*.css", "*.scss", "./src/polyfills.ts"]
}

Étape 4 : Remplacer les dépendances lourdes

Le tree shaking ne peut pas tout faire. Certaines librairies sont structurellement lourdes : elles utilisent CommonJS, regroupent tout dans un seul module, ou embarquent des données dont vous n'avez pas besoin. La solution dans ces cas-là ? Les remplacer purement et simplement.

Les substitutions les plus rentables

Voici les remplacements qui offrent le meilleur rapport effort/gain — et je vous garantis que vous en avez au moins un dans votre projet :

  • moment.js (290 Ko)date-fns (tree-shakeable, ~3 Ko par fonction) ou dayjs (2 Ko). Moment.js inclut toutes ses locales par défaut et n'est pas tree-shakeable. C'est le cas le plus classique de bundle gonflé artificiellement.
  • lodash (71 Ko)lodash-es (même API, tree-shakeable) ou imports directs (lodash/debounce). Mieux encore : beaucoup de fonctions lodash ont des équivalents natifs en JavaScript moderne. Un Array.prototype.flat() fait le même job que _.flatten().
  • faker.js (1,2 Mo)@faker-js/faker avec imports de locales spécifiques. La version originale charge toutes les locales de toutes les langues — oui, toutes.
  • chart.js complet → imports des composants individuels avec l'enregistrement explicite.
// ❌ chart.js complet — inclut tous les types de graphiques
import Chart from "chart.js/auto";

// ✅ chart.js avec imports sélectifs
import {
  Chart,
  LineController,
  LineElement,
  PointElement,
  LinearScale,
  CategoryScale,
  Tooltip,
} from "chart.js";

Chart.register(
  LineController,
  LineElement,
  PointElement,
  LinearScale,
  CategoryScale,
  Tooltip
);

Vérifier la taille d'une dépendance avant de l'installer

Avant d'ajouter une nouvelle dépendance, prenez le réflexe de vérifier son poids avec Bundlephobia (bundlephobia.com). Tapez le nom du package et vous obtenez instantanément sa taille minifiée, gzippée, et son impact estimé sur le temps de chargement. Ça prend 10 secondes et ça peut vous éviter d'embarquer 500 Ko sans le savoir.

Étape 5 : Mettre en place un budget de performance

Optimiser une fois, c'est bien. Mais sans garde-fous, votre bundle va regrossir progressivement à chaque nouvelle fonctionnalité et chaque nouvelle dépendance. C'est ce qu'on appelle la dette de performance — et croyez-moi, elle s'accumule vite.

Configurer les seuils dans Vite

Vite intègre un seuil d'avertissement pour la taille des chunks. Par défaut, il est fixé à 500 Ko (ce qui est franchement trop généreux à mon avis). Vous pouvez et devriez l'ajuster :

// vite.config.ts
export default defineConfig({
  build: {
    chunkSizeWarningLimit: 300, // Avertissement au-dessus de 300 Ko
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            const pkg = id.split("node_modules/")[1].split("/")[0];
            if (["react", "react-dom"].includes(pkg)) return "vendor-react";
            return "vendor";
          }
        },
      },
    },
  },
});

Automatiser avec un script CI

Pour aller plus loin, intégrez la vérification dans votre pipeline CI/CD. Voici un script qui fait échouer le build si un chunk dépasse le budget :

#!/bin/bash
# check-bundle-size.sh
MAX_SIZE_KB=300
BUILD_DIR="dist/assets"

oversized=0
for file in "$BUILD_DIR"/*.js; do
  size_kb=$(( $(wc -c < "$file") / 1024 ))
  if [ "$size_kb" -gt "$MAX_SIZE_KB" ]; then
    echo "⚠️  $(basename $file): ${size_kb} Ko (limite: ${MAX_SIZE_KB} Ko)"
    oversized=1
  fi
done

if [ "$oversized" -eq 1 ]; then
  echo "❌ Budget de performance dépassé. Analysez le bundle avec rollup-plugin-visualizer."
  exit 1
fi

echo "✅ Tous les chunks respectent le budget de ${MAX_SIZE_KB} Ko."

Ce genre de script a sauvé plus d'un projet de la régression silencieuse. Il suffit d'un npm install un peu trop enthousiaste pour dépasser le budget.

Les seuils recommandés en 2026

Pour un site qui vise de bons Core Web Vitals :

  • Bundle JavaScript initial total : moins de 200 Ko (compressé gzip)
  • Chunk individuel maximum : moins de 300 Ko (non compressé)
  • JavaScript total de la page : moins de 500 Ko (compressé gzip)
  • Temps de parsing + compilation JS : moins de 2 secondes sur mobile milieu de gamme

Ces seuils ne sont pas arbitraires. Ils découlent directement des objectifs Core Web Vitals : un LCP inférieur à 2,5 secondes, un INP inférieur à 200 ms, et un TTFB inférieur à 800 ms. Plus votre JavaScript est lourd, plus ces cibles deviennent difficiles à atteindre.

Étape 6 : Compression et livraison optimale

Réduire la taille du code source, c'est une chose. Mais la compression côté serveur est un multiplicateur de gain souvent sous-estimé — et pourtant assez simple à mettre en place.

Brotli vs Gzip

En 2026, Brotli est le format de compression à privilégier. Il produit des fichiers 15 à 25 % plus petits que Gzip pour le contenu textuel (JavaScript, CSS, HTML). Le support navigateur dépasse les 97 %, donc il n'y a plus vraiment de raison de s'en priver. Gzip reste utile comme fallback pour les rares navigateurs qui ne supportent pas Brotli.

Pré-compression des assets avec Vite

Plutôt que de compresser à la volée sur le serveur (ce qui ajoute de la latence à chaque requête), pré-compressez vos assets au moment du build :

// vite.config.ts
import viteCompression from "vite-plugin-compression";

export default defineConfig({
  plugins: [
    // Compression Brotli
    viteCompression({
      algorithm: "brotliCompress",
      ext: ".br",
      threshold: 1024, // Compresser les fichiers > 1 Ko
    }),
    // Compression Gzip en fallback
    viteCompression({
      algorithm: "gzip",
      ext: ".gz",
      threshold: 1024,
    }),
  ],
});

Configurez ensuite votre serveur (Nginx, Caddy, etc.) pour servir les versions pré-compressées quand le navigateur les supporte.

Configuration Nginx pour les assets pré-compressés

# nginx.conf
location /assets/ {
    # Tenter Brotli d'abord, puis Gzip
    gzip_static on;
    brotli_static on;

    # Cache longue durée pour les assets hashés
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Les fichiers produits par Vite incluent un hash dans leur nom (index-a1b2c3d4.js). Ce hash change uniquement quand le contenu change, ce qui rend le cache « immutable » parfaitement sûr. Le navigateur peut mettre en cache pendant un an sans risque de servir du contenu obsolète — c'est élégant et efficace.

Étape 7 : Prefetch et Preload des chunks

Découper le bundle en chunks, c'est très bien. Mais si l'utilisateur clique sur une route et doit attendre le téléchargement du chunk correspondant, l'expérience se dégrade quand même. La solution : anticiper.

Preload des chunks critiques

Vite génère automatiquement des tags <link rel="modulepreload"> pour les imports directs du point d'entrée. Mais pour les routes lazy-loaded, vous pouvez (et devriez) aller plus loin :

<!-- Precharger le chunk d'une route susceptible d'être visitée -->
<link rel="prefetch" href="/assets/Dashboard-a1b2c3d4.js">
<link rel="prefetch" href="/assets/vendor-charts-e5f6g7h8.js">

Prefetch intelligent basé sur la navigation

Plutôt que de prefetcher tous les chunks possibles (ce qui serait contre-productif), préférez une approche ciblée. Par exemple, sur la page de connexion, prefetchez le Dashboard — c'est la destination la plus probable après le login :

// hooks/usePrefetch.ts
import { useEffect } from "react";

export function usePrefetch(routes: string[]) {
  useEffect(() => {
    // Attendre que le navigateur soit idle
    if ("requestIdleCallback" in window) {
      requestIdleCallback(() => {
        routes.forEach((route) => {
          const link = document.createElement("link");
          link.rel = "prefetch";
          link.href = route;
          document.head.appendChild(link);
        });
      });
    }
  }, [routes]);
}

// Utilisation sur la page de connexion
function LoginPage() {
  usePrefetch([
    "/assets/Dashboard-a1b2c3d4.js",
    "/assets/vendor-react-i9j0k1l2.js",
  ]);
  // ...
}

Récapitulatif : checklist d'optimisation du bundle

Allez, on récapitule. Voici votre checklist complète, dans l'ordre de priorité :

  1. Analyser — Lancez rollup-plugin-visualizer pour identifier les modules les plus lourds
  2. Découper — Implémentez le code splitting par route avec les imports dynamiques
  3. Regrouper — Configurez manualChunks pour isoler les vendors en chunks stables et cacheables
  4. Élaguer — Passez à lodash-es, dayjs, et aux imports directs pour un tree shaking efficace
  5. Remplacer — Substituez les dépendances lourdes par des alternatives légères
  6. Compresser — Pré-compressez en Brotli + Gzip au moment du build
  7. Anticiper — Prefetchez les chunks des routes probables
  8. Surveiller — Mettez en place un budget de performance dans votre CI

Chaque étape est incrémentale. Vous n'avez pas besoin de tout faire d'un coup — commencez par l'analyse et le code splitting. Ces deux actions seules peuvent réduire votre bundle initial de 50 % ou plus, et ça se met en place en une heure.

FAQ

Quelle taille de bundle JavaScript est acceptable en 2026 ?

Pour de bons Core Web Vitals, visez moins de 200 Ko de JavaScript compressé (gzip) pour le chargement initial. Le payload total de la page ne devrait pas dépasser 500 Ko compressé. Au-delà, le temps de parsing et d'exécution sur mobile commence à impacter significativement l'INP et le LCP. Utilisez le treemap de rollup-plugin-visualizer pour identifier rapidement les modules à optimiser.

Le code splitting ralentit-il la navigation entre les pages ?

En théorie, oui — chaque changement de route déclenche le téléchargement d'un nouveau chunk. En pratique, avec le prefetching intelligent et la mise en cache du navigateur, l'impact est négligeable. Le gain sur le chargement initial est bien supérieur au léger délai lors de la première navigation. Évitez toutefois de créer des chunks trop petits (en dessous de 30 Ko) : la surcharge réseau des requêtes HTTP supplémentaires annulerait les bénéfices.

Vite fait-il automatiquement le tree shaking ?

Oui, en mode production (vite build), Vite active automatiquement le tree shaking via Rollup. Mais — et c'est un gros « mais » — le tree shaking ne fonctionne qu'avec les modules ES (import/export). Si vos dépendances utilisent CommonJS (require), le tree shaking ne peut pas opérer. Vérifiez toujours que vous importez la version ES de vos librairies (lodash-es au lieu de lodash, par exemple) et que vos propres modules sont déclarés sans effets de bord dans package.json.

Quelle est la différence entre manualChunks en forme objet et en forme fonction ?

La forme objet est plus simple : vous listez explicitement quels packages vont dans quel chunk. La forme fonction offre une logique conditionnelle — vous pouvez inspecter le chemin du module (id) et décider dynamiquement de son assignation. En pratique, commencez par la forme objet pour les cas simples, et passez à la forme fonction quand vous avez besoin de regrouper automatiquement les dépendances par catégorie.

Comment savoir si une dépendance est tree-shakeable avant de l'installer ?

Trois vérifications rapides : (1) le package exporte en format ESM (cherchez "module" ou "exports" dans son package.json), (2) il déclare "sideEffects": false, et (3) il n'utilise pas de barrel files massifs. Bundlephobia est un outil en ligne qui vous donne instantanément la taille d'un package et indique s'il est tree-shakeable. Prenez l'habitude de le consulter avant chaque npm install — ça prend quelques secondes et ça évite les mauvaises surprises.

À propos de l'auteur Editorial Team

Our team of expert writers and editors.