Tutoriel · JavaScript

Tutoriel Webpack 5 : configurer un build existant (Babel, CSS, SASS, PostCSS)

11 min · Débutant · CSS, JavaScript · vérifié le 7 septembre 2026

Réponse rapide

Sur une configuration Webpack 5 reprise aujourd’hui, deux choses cassent silencieusement : node-sass, que sass-loader 17 ne reconnaît plus du tout, et @babel/preset-env sans cible déclarée, qui depuis Babel 8 ne transpile plus rien. Ce guide donne la configuration complète, socle, Babel, CSS, SASS, PostCSS, production, réexécutée le 2 septembre 2026 sur Webpack 5.110.3 et Node 22.

Ce guide part d’un constat simple : en 2026, on ouvre rarement un projet Webpack pour le créer, on l’ouvre pour le réparer. Les quatre briques qui posent problème sont toujours les mêmes — transpiler le JavaScript, charger le CSS, compiler du SCSS, appliquer PostCSS. Elles sont réunies ici, dans une seule configuration testée sur Webpack 5.110.3 et Node 22.

Faut-il encore utiliser Webpack en 2026 ?

La question mérite une réponse franche avant de configurer quoi que ce soit, parce qu’elle décide de l’effort que vous allez investir.

Pour un projet neuf, non. Vite est le choix par défaut de Vue et de Laravel, et c’est aussi celui vers lequel pointe la documentation de la plupart des frameworks front. Démarrer un projet sous Webpack en 2026 revient à écrire à la main une configuration que les autres outils fournissent déjà.

Pour un projet existant, oui, et sans culpabilité. Webpack n’est pas abandonné : la version 5.110.3 est celle utilisée pour ce guide, la feuille de route 2026 annonce le CSS natif, la transpilation TypeScript intégrée et les points d’entrée HTML, et la version 6 est annoncée pour fin 2027. Une base de code avec une configuration lourde, des loaders maison ou des plugins spécifiques n’a aucune raison de migrer dans l’urgence.

La règle pratique : si votre démarrage à froid tient sous cinq secondes, le sujet n’est pas prioritaire. S’il dépasse la trentaine de secondes, la migration devient un vrai gain de confort. La section migrer vers Vite ou Rspack en fin de guide donne les points de départ.

Les vidéos de ce guide datent de la publication initiale, en février 2022. Elles montrent la démarche générale, mais les versions et le code à jour sont ceux du texte ci-dessous.

Le socle : entrée, sortie et serveur de développement

Webpack lit un fichier d’entrée, suit les import de proche en proche et écrit un fichier de sortie. Tout le reste n’est qu’un ajout à ce mécanisme.

bash
mkdir -p src dist
npm init --yes
npm install --save-dev webpack webpack-cli webpack-dev-server

La configuration minimale tient en trois clés. entry désigne le point de départ, output le fichier produit, devServer le dossier servi pendant le développement.

webpack.config.js
const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  devServer: {
    static: { directory: path.resolve(__dirname, 'dist') },
    port: 8080,
  },
};

Deux points sur lesquels les anciennes configurations se trompent souvent. output.path doit être un chemin absolu, d’où le path.resolve(__dirname, …) : une valeur comme ’/dist’ désigne la racine du disque. Et depuis Webpack 5, devServer.static remplace l’ancien contentBase.

Ajoutez les scripts npm, puis lancez le serveur :

package.json
{
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}

Sur la machine de test, npm start répond bien en HTTP 200 sur http://localhost:8080/ avec webpack-dev-server 6.0.0 :

bash
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/, http://[::1]:8080/
<i> [webpack-dev-server] Content not from webpack is served from '/app/dist' directory

Babel : ce qui a changé, et pourquoi votre configuration ne transpile plus

C’est le point le plus important de cette mise à jour, et celui qui casse silencieusement les anciens projets. Babel 8 a changé sa cible par défaut. Une configuration qui déclare @babel/preset-env sans préciser de cible ne transpile plus rien.

bash
npm install --save-dev @babel/core @babel/preset-env babel-loader
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
    ],
  },
};

Voici la configuration Babel telle qu’on la trouve dans la plupart des tutoriels, y compris la version 2022 de cette page :

.babelrc
{
  "presets": ["@babel/preset-env"]
}

Avec Babel 8.0.1, appliquée à ce fichier source :

src/index.js
const nombres = [1, 2, 3, 4];
const doubles = nombres.map((n) => n * 2);
const total = doubles?.reduce((a, b) => a + b, 0) ?? 0;
class Compteur { #valeur = total; lire() { return this.#valeur; } }
document.querySelector('#app').textContent = `Total : ${new Compteur().lire()}`;

elle produit un bundle de 173 octets dans lequel les fonctions fléchées, le chaînage optionnel ?., le coalescement ?? et le champ privé #valeur sont toujours là, intacts :

dist/bundle.js
(()=>{const e=[1,2,3,4].map(e=>2*e),t=e?.reduce((e,t)=>e+t,0)??0;
document.querySelector("#app").textContent=`Total : ${(new class{#e=t;lire(){return this.#e}}).lire()}`})();

Autrement dit, Babel s’exécute, coûte du temps de build, et ne transforme rien. Le correctif tient en une clé : déclarez explicitement la cible.

.babelrc
{
  "presets": [
    ["@babel/preset-env", { "targets": { "ie": "11" } }]
  ]
}

Avec cette cible, le même code produit 1,48 Kio de sortie ES5, avec les fonctions _typeof, _defineProperty et compagnie que l’on attend d’une transpilation réelle.

Il y a une nuance à comprendre plutôt qu’un réflexe à appliquer : avec une cible moderne comme "defaults", la sortie reste identique à l’entrée, et c’est normal. Tous les navigateurs encore suivis gèrent nativement les fonctions fléchées, ?., ?? et les champs privés. Si vous n’avez pas de navigateur ancien à servir, Babel n’a plus grand-chose à faire dans votre chaîne de build. Le retirer est une décision légitime, pas un raccourci.

CSS : charger des feuilles de style depuis JavaScript

Deux loaders travaillent en paire. css-loader résout les @import et les url() et transforme la feuille en module JavaScript ; style-loader injecte ce module dans une balise <style> au chargement de la page.

bash
npm install --save-dev css-loader style-loader
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

L’ordre compte et se lit de droite à gauche : css-loader traite le fichier, puis style-loader injecte le résultat. Inversez les deux et le build échoue.

Il ne reste qu’à importer la feuille depuis le JavaScript :

src/index.js
import './style.css';

Après le build, la couleur déclarée dans style.css se retrouve bien à l’intérieur de dist/bundle.js : en développement, le CSS voyage dans le JavaScript. Pour la production, la section suivante l’extrait dans un vrai fichier .css.

SASS : compiler du SCSS, et se débarrasser de node-sass

Si vous reprenez un projet monté avant 2023, c’est ici que le build va casser. L’instruction historique était npm install --save-dev sass-loader node-sass. Sur Node 22, elle échoue dès l’installation : node-sass n’a pas de binaire pour cette version de Node et sa compilation native s’arrête, sans que sass-loader soit installé non plus :

bash
npm warn deprecated node-sass@9.0.0: Node Sass is no longer supported. Please use `sass` or `sass-embedded` instead.
npm error code 1
npm error path /app/node_modules/node-sass
npm error command failed
npm error command sh -c node scripts/build.js

Et sur un projet où node-sass était déjà présent dans node_modules, c’est le build qui casse, avec une erreur nette et un code de sortie 1 :

bash
ERROR in ./src/style.scss
Module build failed (from ./node_modules/sass-loader/dist/cjs/index.js):
Error: Unknown Sass implementation "node-sass".
    at getSassImplementation (/app/node_modules/sass-loader/dist/cjs/utils.js:168:9)

webpack 5.110.3 compiled with 1 error

Deux choses se sont produites. Le paquet node-sass est déprécié — npm l’annonce lui-même, « Node Sass is no longer supported » — et surtout sass-loader 17 a retiré la prise en charge de cette implémentation. Il n’y a pas de contournement : il faut passer à sass, l’implémentation Dart.

bash
npm uninstall node-sass
npm install --save-dev sass sass-loader
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ],
  },
};

Aucune option n’est nécessaire : sass-loader détecte sass automatiquement. La chaîne se lit toujours de droite à gauche — SCSS compilé en CSS, puis chargé, puis injecté.

src/style.scss
$couleur: #d62828;

#app {
  color: $couleur;
  display: flex;

  &:hover { text-decoration: underline; }
}

Avec sass 1.103.1, le build passe et la variable est bien résolue : color:#d62828 se retrouve dans la sortie.

PostCSS : préfixes et syntaxe moderne

Contrairement à Babel, PostCSS a gardé toute son utilité. Il ajoute les préfixes constructeurs et abaisse la syntaxe CSS récente vers une forme comprise par vos navigateurs cibles.

bash
npm install --save-dev postcss postcss-loader postcss-preset-env
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.s?[ac]ss$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
      },
    ],
  },
};
postcss.config.js
module.exports = {
  plugins: [require('postcss-preset-env')({ stage: 2 })],
};

postcss-loader se place entre css-loader et sass-loader : le SCSS est compilé, PostCSS travaille sur le CSS obtenu, puis css-loader le prend en charge.

Sur la règle de la section précédente, avec user-select et inset ajoutés, la sortie montre le travail réellement effectué :

dist/main.[contenthash].css
#app{color:#d62828;display:flex;-webkit-user-select:none;-moz-user-select:none;
user-select:none;top:0;right:0;bottom:0;left:0}
#app:hover{-webkit-text-decoration:underline;text-decoration:underline}

Les préfixes sont ajoutés et inset: 0 est développé en quatre propriétés. C’est un gain concret, contrairement à Babel sur cible moderne.

Le piège à connaître. Beaucoup de configurations, dont la version 2022 de cette page, passent l’option browsers: ’last 2 versions’ à postcss-preset-env. Cette option est dépréciée, et la requête elle-même est un piège : dans browserslist, last 2 versions signifie « les deux dernières versions de chaque navigateur », Internet Explorer compris. Vérification faite avec caniuse-lite 1.0.30001810, cette requête retourne 30 cibles dont ie 11, ie 10, ie_mob et op_mini all. Résultat mesuré sur le même fichier :

css
/* avec browsers: 'last 2 versions' — IE inclus */
display:-webkit-box; display:-ms-flexbox; display:flex;

/* avec la cible par défaut (defaults) — 32 cibles, aucun IE */
display:flex;

La bonne pratique est de supprimer l’option browsers et de déclarer un champ browserslist dans le package.json, lu par PostCSS, Babel et le reste de la chaîne :

package.json
{
  "browserslist": ["defaults", "not dead"]
}

Passer en production : extraction du CSS, HTML généré, hachage

Trois ajouts séparent la configuration de développement d’un build livrable. mini-css-extract-plugin sort le CSS du bundle JavaScript dans un fichier autonome. html-webpack-plugin génère le index.html et y injecte les bons noms de fichiers. Le [contenthash] et output.clean gèrent le cache et le ménage.

bash
npm install --save-dev mini-css-extract-plugin html-webpack-plugin

Voici la configuration complète, celle qui a servi à toutes les mesures de cette page :

webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const enProduction = process.env.NODE_ENV === 'production';

module.exports = {
  mode: enProduction ? 'production' : 'development',
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: enProduction ? '[name].[contenthash].js' : '[name].js',
    clean: true,
  },
  module: {
    rules: [
      { test: /\.m?js$/, exclude: /node_modules/, use: 'babel-loader' },
      {
        test: /\.s?[ac]ss$/i,
        use: [
          enProduction ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'src/index.html') }),
    ...(enProduction ? [new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' })] : []),
  ],
  devServer: {
    static: { directory: path.resolve(__dirname, 'dist') },
    port: 8080,
    hot: true,
  },
};

Le point à retenir : MiniCssExtractPlugin.loader et style-loader s’excluent mutuellement. On garde style-loader en développement, pour le rechargement à chaud, et on bascule sur l’extraction en production.

bash
NODE_ENV=production npx webpack
bash
asset index.html 240 bytes [emitted] [minimized]
asset main.ca8a5113b21c83c25e3e.css 198 bytes [emitted] [immutable] (name: main)
asset main.b0cab13462d9bb7b12ba.js 99 bytes [emitted] [immutable] (name: main)
webpack 5.110.3 compiled successfully

Le HTML produit référence les deux fichiers hachés, sans intervention manuelle :

dist/index.html
<!doctype html><html lang=fr><head><meta charset=utf-8><title>Hello Webpack</title>
<script defer src=main.b0cab13462d9bb7b12ba.js></script>
<link href=main.ca8a5113b21c83c25e3e.css rel=stylesheet></head>
<body><div id=app></div></body></html>

L’alternative sans loader : le CSS natif de Webpack

Webpack sait charger du CSS sans aucun loader, via une option expérimentale. C’est l’un des chantiers annoncés dans la feuille de route 2026, destiné à devenir le comportement par défaut en version 6.

webpack.config.js
module.exports = {
  mode: 'production',
  entry: path.resolve(__dirname, 'src/index.js'),
  output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', clean: true },
  experiments: { css: true },
};

Sans css-loader, sans style-loader et sans mini-css-extract-plugin, le build produit bien un fichier CSS séparé :

bash
asset bundle.js 71 bytes [emitted] [minimized] (name: main)
asset bundle.css 32 bytes [emitted] [minimized] (name: main)
webpack 5.110.3 compiled successfully in 1392 ms

À réserver aux projets qui n’ont besoin que de CSS simple : l’option reste expérimentale, et elle ne remplace ni SASS ni PostCSS, qui exigent toujours leurs loaders.

Migrer vers Vite ou Rspack : par où commencer

Si le diagnostic de la première section vous oriente vers une migration, deux chemins existent, et ils n’ont pas le même coût.

Rspack est une réécriture de Webpack en Rust qui conserve le modèle de configuration et une large compatibilité avec l’API des plugins. C’est le chemin le moins coûteux quand la configuration existante est lourde : les concepts d’entry, d’output et de règles de loaders vus dans ce guide restent valables.

Vite impose de repenser la configuration, mais c’est la cible par défaut de l’écosystème. La migration se joue surtout sur les plugins spécifiques et les imports non standards. Les guides français cités en sources font état d’une journée de travail sur un projet Vue de taille moyenne.

Dans les deux cas, le travail de ce guide n’est pas perdu : savoir ce que fait précisément chaque loader de votre configuration actuelle est exactement ce qui permet de décider ce qu’il faut reporter, et ce qu’il faut abandonner.

Ce guide fait partie des fondations rassemblées sur le hub Développement web.

Erreurs fréquentes

node-sass arrête le build sass-loader 17 a retiré cette implémentation : le build échoue sur « Unknown Sass implementation "node-sass" » avec un code de sortie 1. Remplacez-la par le paquet sass.
@babel/preset-env sans targets ne transpile plus Depuis Babel 8, la cible par défaut a changé : le code moderne ressort intact. Déclarez targets ou un champ browserslist.
browsers: 'last 2 versions' fait entrer Internet Explorer Dans browserslist, cette requête retourne 30 cibles dont ie 11 et ie 10, d’où des préfixes -ms-flexbox inutiles. L’option browsers est de toute façon dépréciée.
style-loader et MiniCssExtractPlugin dans la même règle Les deux s’excluent. Gardez style-loader en développement et basculez sur l’extraction en production.
output.path doit être absolu Une valeur comme '/dist' désigne la racine du disque. Utilisez path.resolve(__dirname, 'dist').
L’ordre des loaders se lit de droite à gauche ['style-loader', 'css-loader'] exécute d’abord css-loader. Inversé, le build échoue.
Newsletter

Les nouveaux tests, tutoriels et projets, par e-mail.

Tests reproductibles, code versionné, résultats datés. Jamais de spam.