Webpack 5 Tutorial: bestehenden Build reparieren (Babel, CSS, SASS)
10 Min. · Anfänger · CSS, JavaScript · geprüft am 7 September 2026
An einer heute übernommenen Webpack-5-Konfiguration brechen zwei Dinge stillschweigend: node-sass, das sass-loader 17 überhaupt nicht mehr kennt, und @babel/preset-env ohne deklariertes Target, das seit Babel 8 gar nichts mehr transpiliert. Dieser Guide liefert die vollständige Konfiguration, Fundament, Babel, CSS, SASS, PostCSS, Produktion, , am 2. September 2026 erneut ausgeführt mit Webpack 5.110.3 und Node 22.
Dieser Guide geht von einer schlichten Beobachtung aus: 2026 öffnet man ein Webpack-Projekt selten, um es aufzusetzen — man öffnet es, um es zu reparieren. Die vier Bausteine, die Ärger machen, sind immer dieselben: JavaScript transpilieren, CSS laden, SCSS kompilieren, PostCSS anwenden. Hier stehen sie zusammen, in einer einzigen Konfiguration, getestet mit Webpack 5.110.3 und Node 22.
Lohnt sich Webpack 2026 überhaupt noch?
Die Frage verdient eine klare Antwort, bevor du irgendetwas konfigurierst — von ihr hängt ab, wie viel Aufwand sich lohnt.
Für ein neues Projekt: nein. Vite ist die Standardwahl von Vue und Laravel, und auch die Dokumentation der meisten Frontend-Frameworks verweist darauf. Ein Projekt 2026 mit Webpack zu starten heißt, eine Konfiguration von Hand zu schreiben, die andere Tools längst mitliefern.
Für ein bestehendes Projekt: ja, und ohne schlechtes Gewissen. Webpack ist nicht aufgegeben: Version 5.110.3 kommt in diesem Guide zum Einsatz, die Roadmap 2026 kündigt natives CSS, integrierte TypeScript-Transpilation und HTML-Einstiegspunkte an, und Version 6 ist für Ende 2027 angesetzt. Eine Codebasis mit schwerer Konfiguration, eigenen Loadern oder speziellen Plugins hat keinen Grund, überstürzt zu migrieren.
Die Faustregel: Bleibt dein Kaltstart unter fünf Sekunden, ist das Thema nicht dringend. Dauert er über dreißig Sekunden, bringt die Migration spürbar Komfort. Der Abschnitt zu Vite oder Rspack migrieren am Ende des Guides nennt die Einstiegspunkte.
Die Videos in diesem Guide stammen aus der Erstveröffentlichung im Februar 2022. Sie zeigen das grundsätzliche Vorgehen; aktuell sind die Versionen und der Code im Text darunter.
Das Fundament: Entry, Output und Dev-Server
Webpack liest eine Einstiegsdatei, verfolgt die import von Modul zu Modul und schreibt eine Ausgabedatei. Alles Weitere ist nur Beiwerk zu diesem Mechanismus.
mkdir -p src dist
npm init --yes
npm install --save-dev webpack webpack-cli webpack-dev-serverDie Minimalkonfiguration besteht aus drei Schlüsseln. entry benennt den Startpunkt, output die erzeugte Datei, devServer das Verzeichnis, das während der Entwicklung ausgeliefert wird.
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,
},
};Zwei Punkte, an denen alte Konfigurationen regelmäßig danebenliegen. output.path muss ein absoluter Pfad sein, daher das path.resolve(__dirname, …): Ein Wert wie ’/dist’ zeigt auf die Wurzel der Festplatte. Und seit Webpack 5 ersetzt devServer.static das alte contentBase.
Ergänze die npm-Skripte und starte dann den Server:
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}Auf der Testmaschine antwortet npm start mit webpack-dev-server 6.0.0 sauber mit HTTP 200 auf http://localhost:8080/:
<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' directoryBabel: was sich geändert hat und warum deine Konfiguration nicht mehr transpiliert
Das ist der wichtigste Punkt dieser Aktualisierung — und der, der alte Projekte stillschweigend kaputt macht. Babel 8 hat sein Standard-Target geändert. Eine Konfiguration, die @babel/preset-env ohne Target deklariert, transpiliert gar nichts mehr.
npm install --save-dev @babel/core @babel/preset-env babel-loadermodule.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};So sieht die Babel-Konfiguration aus, wie sie in den meisten Tutorials steht — auch in der Fassung dieser Seite von 2022:
{
"presets": ["@babel/preset-env"]
}Mit Babel 8.0.1, angewendet auf diese Quelldatei:
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()}`;erzeugt sie ein Bundle von 173 Bytes, in dem Arrow-Functions, der Optional-Chaining-Operator ?., der Nullish-Operator ?? und das private Feld #valeur unverändert stehen bleiben:
(()=>{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()}`})();Anders gesagt: Babel läuft, kostet Build-Zeit und verändert nichts. Die Korrektur besteht aus einem einzigen Schlüssel — deklariere das Target explizit.
{
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }]
]
}Mit diesem Target erzeugt derselbe Code 1,48 KiB ES5-Ausgabe, samt den Hilfsfunktionen _typeof, _defineProperty und Konsorten, die man von einer echten Transpilation erwartet.
Hier ist Verständnis gefragt, kein Reflex: Mit einem modernen Target wie "defaults" bleibt die Ausgabe identisch mit der Eingabe, und das ist richtig so. Alle noch gepflegten Browser beherrschen Arrow-Functions, ?., ?? und private Felder von Haus aus. Wenn du keine alten Browser bedienen musst, hat Babel in deiner Build-Kette nicht mehr viel zu suchen. Es zu entfernen ist eine legitime Entscheidung, keine Abkürzung.
CSS: Stylesheets aus JavaScript heraus laden
Zwei Loader arbeiten im Tandem. css-loader löst @import und url() auf und verwandelt das Stylesheet in ein JavaScript-Modul; style-loader fügt dieses Modul beim Laden der Seite in ein <style>-Tag ein.
npm install --save-dev css-loader style-loadermodule.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};Die Reihenfolge zählt und wird von rechts nach links gelesen: css-loader verarbeitet die Datei, dann setzt style-loader das Ergebnis ein. Vertauscht schlägt der Build fehl.
Bleibt nur noch, das Stylesheet aus dem JavaScript zu importieren:
import './style.css';Nach dem Build steckt die in style.css deklarierte Farbe tatsächlich in dist/bundle.js: In der Entwicklung reist das CSS im JavaScript mit. Für die Produktion zieht der nächste Abschnitt es in eine echte .css-Datei heraus.
SASS: SCSS kompilieren und node-sass loswerden
Wenn Sie ein vor 2023 aufgesetztes Projekt übernehmen, bricht der Build hier. Die historische Anweisung lautete npm install --save-dev sass-loader node-sass. Unter Node 22 scheitert sie schon bei der Installation: node-sass hat kein Binary für diese Node-Version und der native Build bricht ab, ohne dass sass-loader installiert würde:
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.jsUnd in einem Projekt, in dem node-sass bereits in node_modules lag, bricht der Build, mit einer klaren Fehlermeldung und Exit-Code 1:
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 errorZwei Dinge sind passiert. Das Paket node-sass ist veraltet — npm sagt es selbst: „Node Sass is no longer supported“ — und vor allem hat sass-loader 17 die Unterstützung dieser Implementierung entfernt. Es gibt keinen Umweg: Man muss zu sass, der Dart-Implementierung, wechseln.
npm uninstall node-sass
npm install --save-dev sass sass-loadermodule.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
],
},
};Optionen braucht es keine: sass-loader erkennt sass automatisch. Die Kette liest sich weiterhin von rechts nach links — SCSS zu CSS kompiliert, dann geladen, dann eingefügt.
$couleur: #d62828;
#app {
color: $couleur;
display: flex;
&:hover { text-decoration: underline; }
}Mit sass 1.103.1 läuft der Build durch und die Variable wird sauber aufgelöst: color:#d62828 steht in der Ausgabe.
PostCSS: Präfixe und moderne Syntax
Anders als Babel hat PostCSS seinen Nutzen voll behalten. Es ergänzt Herstellerpräfixe und übersetzt neuere CSS-Syntax in eine Form, die deine Zielbrowser verstehen.
npm install --save-dev postcss postcss-loader postcss-preset-envmodule.exports = {
module: {
rules: [
{
test: /\.s?[ac]ss$/i,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
},
],
},
};module.exports = {
plugins: [require('postcss-preset-env')({ stage: 2 })],
};postcss-loader steht zwischen css-loader und sass-loader: Das SCSS wird kompiliert, PostCSS arbeitet auf dem entstandenen CSS, dann übernimmt css-loader.
Auf der Regel aus dem vorigen Abschnitt, ergänzt um user-select und inset, zeigt die Ausgabe, was wirklich passiert:
#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}Die Präfixe werden gesetzt und inset: 0 wird in vier Eigenschaften ausgeschrieben. Das ist ein konkreter Gewinn — anders als bei Babel mit modernem Target.
Die Falle, die man kennen sollte. Viele Konfigurationen, die Fassung dieser Seite von 2022 eingeschlossen, übergeben postcss-preset-env die Option browsers: ’last 2 versions’. Diese Option ist deprecated, und die Query selbst ist eine Falle: In browserslist bedeutet last 2 versions „die letzten beiden Versionen jedes Browsers“, Internet Explorer inklusive. Geprüft mit caniuse-lite 1.0.30001810, liefert diese Query 30 Targets, darunter ie 11, ie 10, ie_mob und op_mini all. Gemessenes Ergebnis auf derselben Datei:
/* mit browsers: 'last 2 versions' — IE inbegriffen */
display:-webkit-box; display:-ms-flexbox; display:flex;
/* mit dem Standard-Target (defaults) — 32 Targets, kein IE */
display:flex;Die gute Praxis: die Option browsers streichen und stattdessen in der package.json ein Feld browserslist deklarieren, das PostCSS, Babel und der Rest der Kette lesen:
{
"browserslist": ["defaults", "not dead"]
}Ab in die Produktion: CSS extrahieren, HTML erzeugen, Hashes setzen
Drei Ergänzungen trennen die Entwicklungskonfiguration von einem auslieferbaren Build. mini-css-extract-plugin holt das CSS aus dem JavaScript-Bundle in eine eigene Datei. html-webpack-plugin erzeugt die index.html und trägt die richtigen Dateinamen ein. [contenthash] und output.clean kümmern sich um Cache und Aufräumen.
npm install --save-dev mini-css-extract-plugin html-webpack-pluginHier die vollständige Konfiguration — dieselbe, die allen Messungen auf dieser Seite zugrunde liegt:
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,
},
};Merksatz: MiniCssExtractPlugin.loader und style-loader schließen einander aus. In der Entwicklung bleibt style-loader, wegen des Hot Reloadings, in der Produktion wird auf die Extraktion umgeschaltet.
NODE_ENV=production npx webpackasset 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 successfullyDas erzeugte HTML referenziert beide gehashten Dateien, ganz ohne Handarbeit:
<!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>Die Alternative ohne Loader: natives CSS in Webpack
Webpack kann CSS ohne jeden Loader laden, über eine experimentelle Option. Das ist eine der Baustellen aus der Roadmap 2026 und soll in Version 6 zum Standardverhalten werden.
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 },
};Ohne css-loader, ohne style-loader und ohne mini-css-extract-plugin erzeugt der Build tatsächlich eine separate CSS-Datei:
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 msNur für Projekte, die schlichtes CSS brauchen: Die Option bleibt experimentell und ersetzt weder SASS noch PostCSS, die nach wie vor ihre Loader verlangen.
Zu Vite oder Rspack migrieren: wo anfangen
Wenn die Diagnose aus dem ersten Abschnitt auf eine Migration hinausläuft, gibt es zwei Wege — und sie kosten nicht dasselbe.
Rspack ist eine Neuimplementierung von Webpack in Rust, die das Konfigurationsmodell und eine weitgehende Kompatibilität mit der Plugin-API behält. Das ist der günstigere Weg, wenn die bestehende Konfiguration schwer ist: Die Konzepte entry, output und die Loader-Regeln aus diesem Guide gelten weiter.
Vite zwingt dazu, die Konfiguration neu zu denken, ist aber das Standardziel des Ökosystems. Die Migration entscheidet sich vor allem an speziellen Plugins und nicht standardkonformen Imports. Die als Quelle genannten französischen Guides sprechen von einem Arbeitstag für ein mittelgroßes Vue-Projekt.
In beiden Fällen ist die Arbeit aus diesem Guide nicht umsonst: Genau zu wissen, was jeder Loader in deiner aktuellen Konfiguration tut, ist die Voraussetzung dafür, zu entscheiden, was mitkommt und was zurückbleibt.
Dieser Guide gehört zu den Grundlagen, die im Hub Webentwicklung gesammelt sind.
Häufige Fehler
sass-loader 17 hat diese Implementierung entfernt: Der Build scheitert an „Unknown Sass implementation "node-sass"“ mit Exit-Code 1. Ersetze sie durch das Paket sass.targets oder ein Feld browserslist.ie 11 und ie 10, und damit nutzlose -ms-flexbox-Präfixe. Die Option browsers ist ohnehin deprecated.style-loader in der Entwicklung und schalte in der Produktion auf die Extraktion um.'/dist' zeigt auf die Wurzel der Festplatte. Nimm path.resolve(__dirname, 'dist').['style-loader', 'css-loader'] führt zuerst css-loader aus. Umgedreht scheitert der Build.