Poradnik · JavaScript

Webpack 5: konfiguracja builda — Babel, CSS, SASS i PostCSS

10 min · Początkujący · CSS, JavaScript · zweryfikowano 7 września 2026

Szybka odpowiedź

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.

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

Minimalna konfiguracja mieści się w trzech kluczach. entry wskazuje punkt startowy, output plik wynikowy, devServer katalog serwowany podczas developmentu.

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,
  },
};

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:

package.json
{
  "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:

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: 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.

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',
      },
    ],
  },
};

Oto konfiguracja Babela w takiej postaci, w jakiej występuje w większości tutoriali — również w wersji tej strony z 2022 roku:

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

Z Babelem 8.0.1, zastosowana do tego pliku źródłowego:

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()}`;

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:

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()}`})();

Innymi słowy: Babel się wykonuje, kosztuje czas builda i nic nie przekształca. Poprawka mieści się w jednym kluczu — zadeklaruj cel wprost.

.babelrc
{
  "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.

bash
npm install --save-dev css-loader style-loader
webpack.config.js
module.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:

src/index.js
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:

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

A 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:

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

Stał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.

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'],
      },
    ],
  },
};

Ż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.

src/style.scss
$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.

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 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ę:

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}

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:

css
/* 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:

package.json
{
  "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.

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

Oto pełna konfiguracja, ta sama, która posłużyła do wszystkich pomiarów na tej stronie:

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,
  },
};

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ę.

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

Wygenerowany HTML odwołuje się do obu zahashowanych plików, bez ręcznej interwencji:

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>

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.

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 },
};

Bez css-loader, bez style-loader i bez mini-css-extract-plugin build faktycznie tworzy osobny plik CSS:

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

Rozwią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

node-sass zatrzymuje build 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.
@babel/preset-env bez targets już nie transpiluje Od Babela 8 domyślny cel się zmienił: nowoczesny kod wychodzi nietknięty. Zadeklaruj targets albo pole browserslist.
browsers: 'last 2 versions' wpuszcza Internet Explorera W browserslist to zapytanie zwraca 30 celów, w tym ie 11 i ie 10, stąd zbędne prefiksy -ms-flexbox. Opcja browsers i tak jest przestarzała.
style-loader i MiniCssExtractPlugin w jednej regule Oba się wykluczają. Zostaw style-loader w developmencie i przełącz się na ekstrakcję na produkcji.
output.path musi być bezwzględne Wartość w rodzaju '/dist' wskazuje katalog główny dysku. Użyj path.resolve(__dirname, 'dist').
Kolejność loaderów czyta się od prawej do lewej ['style-loader', 'css-loader'] uruchamia najpierw css-loader. Odwrotnie build się wywala.
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.