Webpack 5: konfiguracja builda — Babel, CSS, SASS i PostCSS
10 min · Początkujący · CSS, JavaScript · zweryfikowano 7 września 2026
W przejmowanej dziś konfiguracji Webpack 5 dwie rzeczy psują się po cichu: node-sass, którego sass-loader 17 w ogóle już nie rozpoznaje, oraz @babel/preset-env bez zadeklarowanego celu, który od Babela 8 nie transpiluje niczego. Ten przewodnik podaje pełną konfigurację, podstawa, Babel, CSS, SASS, PostCSS, produkcja, uruchomioną ponownie 2 września 2026 roku na Webpacku 5.110.3 i Node 22.
Ten przewodnik wychodzi od prostej obserwacji: w 2026 roku projekt na Webpacku rzadko otwiera się po to, żeby go stworzyć — otwiera się go po to, żeby go naprawić. Kłopot sprawiają zawsze te same cztery elementy: transpilacja JavaScriptu, ładowanie CSS, kompilacja SCSS i PostCSS. Tutaj są zebrane w jednej konfiguracji, sprawdzonej na Webpacku 5.110.3 i Node 22.
Czy w 2026 roku warto jeszcze używać Webpacka?
To pytanie zasługuje na szczerą odpowiedź, zanim cokolwiek skonfigurujesz, bo od niej zależy, ile wysiłku w to włożysz.
W nowym projekcie — nie. Vite jest domyślnym wyborem Vue i Laravela, a dokumentacja większości frontendowych frameworków kieruje właśnie tam. Zakładanie projektu na Webpacku w 2026 roku oznacza pisanie ręcznie konfiguracji, którą inne narzędzia dostarczają od razu.
W istniejącym projekcie — tak, i bez wyrzutów sumienia. Webpack nie został porzucony: ten przewodnik powstał na wersji 5.110.3, roadmapa na 2026 rok zapowiada natywny CSS, wbudowaną transpilację TypeScriptu i punkty wejścia w HTML, a wersja 6 jest planowana na koniec 2027 roku. Kod z rozbudowaną konfiguracją, własnymi loaderami czy specyficznymi wtyczkami nie ma powodu migrować w pośpiechu.
Reguła praktyczna: jeśli zimny start mieści się w pięciu sekundach, temat nie jest pilny. Jeśli przekracza trzydzieści sekund, migracja daje realny zysk komfortu. Sekcja migracja do Vite lub Rspacka na końcu przewodnika podaje punkty wyjścia.
Filmy w tym przewodniku pochodzą z pierwszej publikacji, z lutego 2022 roku. Pokazują ogólne podejście, ale aktualne wersje i kod znajdziesz w tekście poniżej.
Podstawa: wejście, wyjście i serwer deweloperski
Webpack czyta plik wejściowy, śledzi kolejne import i zapisuje plik wyjściowy. Cała reszta to tylko dodatki do tego mechanizmu.
mkdir -p src dist
npm init --yes
npm install --save-dev webpack webpack-cli webpack-dev-serverMinimalna konfiguracja mieści się w trzech kluczach. entry wskazuje punkt startowy, output plik wynikowy, devServer katalog serwowany podczas developmentu.
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,
},
};Dwie rzeczy, na których stare konfiguracje wykładają się najczęściej. output.path musi być ścieżką bezwzględną, stąd path.resolve(__dirname, …): wartość w rodzaju '/dist' wskazuje katalog główny dysku. A od Webpacka 5 devServer.static zastępuje dawne contentBase.
Dodaj skrypty npm, a potem uruchom serwer:
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}Na maszynie testowej npm start odpowiada kodem HTTP 200 pod adresem http://localhost:8080/, z webpack-dev-server 6.0.0:
<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: co się zmieniło i dlaczego twoja konfiguracja przestała transpilować
To najważniejszy punkt tej aktualizacji i ten, który po cichu psuje stare projekty. Babel 8 zmienił domyślny cel transpilacji. Konfiguracja, która deklaruje @babel/preset-env bez podania celu, nie transpiluje już niczego.
npm install --save-dev @babel/core @babel/preset-env babel-loadermodule.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};Oto konfiguracja Babela w takiej postaci, w jakiej występuje w większości tutoriali — również w wersji tej strony z 2022 roku:
{
"presets": ["@babel/preset-env"]
}Z Babelem 8.0.1, zastosowana do tego pliku źródłowego:
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()}`;daje bundle o wielkości 173 bajtów, w którym funkcje strzałkowe, opcjonalne łańcuchy ?., operator ?? i pole prywatne #valeur zostają nietknięte:
(()=>{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()}`})();Innymi słowy: Babel się wykonuje, kosztuje czas builda i nic nie przekształca. Poprawka mieści się w jednym kluczu — zadeklaruj cel wprost.
{
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }]
]
}Z tym celem ten sam kod daje 1,48 KiB wyjścia w ES5, z funkcjami _typeof, _defineProperty i resztą, których oczekuje się po prawdziwej transpilacji.
Jest tu niuans, który warto zrozumieć, zamiast działać odruchowo: przy nowoczesnym celu w rodzaju "defaults" wyjście pozostaje identyczne z wejściem i to jest normalne. Wszystkie wciąż wspierane przeglądarki natywnie obsługują funkcje strzałkowe, ?., ?? i pola prywatne. Jeśli nie musisz obsługiwać starych przeglądarek, Babel nie ma już wiele do roboty w twoim łańcuchu budowania. Usunięcie go to uprawniona decyzja, a nie pójście na skróty.
CSS: ładowanie arkuszy stylów z poziomu JavaScriptu
Dwa loadery pracują w parze. css-loader rozwiązuje @import oraz url() i zamienia arkusz w moduł JavaScriptu; style-loader wstrzykuje ten moduł w znacznik <style> przy ładowaniu strony.
npm install --save-dev css-loader style-loadermodule.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};Kolejność ma znaczenie i czyta się ją od prawej do lewej: css-loader przetwarza plik, potem style-loader wstrzykuje wynik. Zamień je miejscami, a build się wywali.
Zostaje tylko zaimportować arkusz z JavaScriptu:
import './style.css';Po buildzie kolor zadeklarowany w style.css faktycznie ląduje w środku dist/bundle.js: w trybie deweloperskim CSS podróżuje wewnątrz JavaScriptu. Na produkcji kolejna sekcja wyciąga go do prawdziwego pliku .css.
SASS: kompilacja SCSS i pożegnanie z node-sass
Jeśli przejmujesz projekt skonfigurowany przed 2023 r., to tutaj build się psuje. Historyczna instrukcja brzmiała npm install --save-dev sass-loader node-sass. Na Node 22 zawodzi już przy instalacji: node-sass nie ma binarki dla tej wersji Node i jego natywna kompilacja się zatrzymuje, a sass-loader też nie zostaje zainstalowany:
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.jsA w projekcie, w którym node-sass był już obecny w node_modules, psuje się build, z wyraźnym błędem i kodem wyjścia 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 errorStały się dwie rzeczy. Pakiet node-sass jest przestarzały — npm sam to ogłasza: „Node Sass is no longer supported” — a przede wszystkim sass-loader 17 usunął obsługę tej implementacji. Nie ma obejścia: trzeba przejść na sass, implementację w Dart.
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'],
},
],
},
};Żadna opcja nie jest potrzebna: sass-loader wykrywa sass automatycznie. Łańcuch nadal czyta się od prawej do lewej — SCSS skompilowany do CSS, potem załadowany, potem wstrzyknięty.
$couleur: #d62828;
#app {
color: $couleur;
display: flex;
&:hover { text-decoration: underline; }
}Z sass 1.103.1 build przechodzi, a zmienna jest poprawnie rozwiązana: color:#d62828 pojawia się na wyjściu.
PostCSS: prefiksy i nowoczesna składnia
W przeciwieństwie do Babela PostCSS zachował pełną przydatność. Dodaje prefiksy producentów i sprowadza świeżą składnię CSS do postaci rozumianej przez twoje docelowe przeglądarki.
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 ustawia się między css-loader a sass-loader: SCSS zostaje skompilowany, PostCSS pracuje na powstałym CSS, potem przejmuje go css-loader.
Na regule z poprzedniej sekcji, z dodanymi user-select i inset, wyjście pokazuje realnie wykonaną pracę:
#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}Prefiksy zostały dodane, a inset: 0 rozwinięty do czterech właściwości. To konkretny zysk, inaczej niż w przypadku Babela z nowoczesnym celem.
Pułapka, o której trzeba wiedzieć. Wiele konfiguracji, w tym wersja tej strony z 2022 roku, przekazuje do postcss-preset-env opcję browsers: 'last 2 versions'. Ta opcja jest przestarzała, a samo zapytanie to pułapka: w browserslist last 2 versions oznacza „dwie ostatnie wersje każdej przeglądarki”, łącznie z Internet Explorerem. Sprawdzone na caniuse-lite 1.0.30001810: zapytanie zwraca 30 celów, w tym ie 11, ie 10, ie_mob i op_mini all. Wynik zmierzony na tym samym pliku:
/* z browsers: 'last 2 versions' — z IE włącznie */
display:-webkit-box; display:-ms-flexbox; display:flex;
/* z domyślnym celem (defaults) — 32 cele, żadnego IE */
display:flex;Dobra praktyka to usunięcie opcji browsers i zadeklarowanie pola browserslist w package.json, które czytają PostCSS, Babel i reszta łańcucha:
{
"browserslist": ["defaults", "not dead"]
}Wejście na produkcję: ekstrakcja CSS, generowany HTML, hashowanie
Konfigurację deweloperską od builda gotowego do wdrożenia dzielą trzy dodatki. mini-css-extract-plugin wyciąga CSS z bundle’a JavaScriptu do osobnego pliku. html-webpack-plugin generuje index.html i wstrzykuje w niego właściwe nazwy plików. [contenthash] i output.clean zajmują się cache’em i sprzątaniem.
npm install --save-dev mini-css-extract-plugin html-webpack-pluginOto pełna konfiguracja, ta sama, która posłużyła do wszystkich pomiarów na tej stronie:
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,
},
};Rzecz do zapamiętania: MiniCssExtractPlugin.loader i style-loader wykluczają się nawzajem. W trybie deweloperskim zostaje style-loader, dla przeładowywania na gorąco, a na produkcji przechodzi się na ekstrakcję.
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 successfullyWygenerowany HTML odwołuje się do obu zahashowanych plików, bez ręcznej interwencji:
<!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>Alternatywa bez loaderów: natywny CSS w Webpacku
Webpack potrafi ładować CSS bez żadnego loadera, przez opcję eksperymentalną. To jeden z tematów zapowiedzianych w roadmapie na 2026 rok; ma stać się zachowaniem domyślnym w wersji 6.
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 },
};Bez css-loader, bez style-loader i bez mini-css-extract-plugin build faktycznie tworzy osobny plik CSS:
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 msRozwiązanie dla projektów, którym wystarcza prosty CSS: opcja pozostaje eksperymentalna i nie zastępuje ani SASS-a, ani PostCSS, które nadal wymagają swoich loaderów.
Migracja do Vite lub Rspacka: od czego zacząć
Jeśli diagnoza z pierwszej sekcji kieruje cię ku migracji, drogi są dwie i nie kosztują tyle samo.
Rspack to napisany w Ruście odpowiednik Webpacka, który zachowuje model konfiguracji i szeroką zgodność z API wtyczek. To najtańsza droga, kiedy istniejąca konfiguracja jest rozbudowana: pojęcia entry, output i reguł loaderów omówione w tym przewodniku pozostają aktualne.
Vite wymaga przemyślenia konfiguracji od nowa, ale to domyślny cel całego ekosystemu. Migracja rozgrywa się głównie na specyficznych wtyczkach i niestandardowych importach. Francuskie przewodniki wymienione w źródłach mówią o jednym dniu pracy przy średniej wielkości projekcie na Vue.
W obu przypadkach praca włożona w ten przewodnik nie idzie na marne: wiedza o tym, co dokładnie robi każdy loader w twojej obecnej konfiguracji, jest właśnie tym, co pozwala zdecydować, co przenieść, a co porzucić.
Ten przewodnik należy do fundamentów zebranych w hubie Programowanie webowe.
Częste błędy
sass-loader 17 usunął tę implementację: build kończy się komunikatem Unknown Sass implementation "node-sass" i kodem wyjścia 1. Zastąp ją pakietem sass.targets albo pole browserslist.ie 11 i ie 10, stąd zbędne prefiksy -ms-flexbox. Opcja browsers i tak jest przestarzała.style-loader w developmencie i przełącz się na ekstrakcję na produkcji.'/dist' wskazuje katalog główny dysku. Użyj path.resolve(__dirname, 'dist').['style-loader', 'css-loader'] uruchamia najpierw css-loader. Odwrotnie build się wywala.