Webpack 5: configurare una build esistente con Babel, SASS e PostCSS
10 min · Principiante · CSS, JavaScript · verificato il 7 Settembre 2026
In una configurazione Webpack 5 ripresa oggi, due cose si rompono in silenzio: node-sass, che sass-loader 17 non riconosce più affatto, e @babel/preset-env senza target dichiarato, che da Babel 8 non transpila più nulla. Questa guida dà la configurazione completa, basi, Babel, CSS, SASS, PostCSS, produzione, rieseguita il 2 settembre 2026 su Webpack 5.110.3 e Node 22.
Questa guida parte da una constatazione semplice: nel 2026 un progetto Webpack lo apri raramente per crearlo, lo apri per ripararlo. I quattro pezzi che danno problemi sono sempre gli stessi — transpilare il JavaScript, caricare il CSS, compilare lo SCSS, applicare PostCSS. Qui sono riuniti in un’unica configurazione testata su Webpack 5.110.3 e Node 22.
Ha ancora senso usare Webpack nel 2026?
La domanda merita una risposta netta prima di configurare qualsiasi cosa, perché decide quanto lavoro ha senso investire.
Per un progetto nuovo, no. Vite è la scelta predefinita di Vue e di Laravel, ed è anche quella verso cui punta la documentazione della maggior parte dei framework front-end. Avviare un progetto su Webpack nel 2026 significa scrivere a mano una configurazione che gli altri strumenti forniscono già.
Per un progetto esistente, sì, e senza sensi di colpa. Webpack non è abbandonato: la versione 5.110.3 è quella usata per questa guida, la roadmap 2026 annuncia il CSS nativo, la transpilazione TypeScript integrata e i punti di ingresso HTML, e la versione 6 è attesa per fine 2027. Un codebase con una configurazione pesante, loader fatti in casa o plugin specifici non ha nessun motivo di migrare di corsa.
La regola pratica: se l’avvio a freddo sta sotto i cinque secondi, l’argomento non è prioritario. Se supera la trentina di secondi, la migrazione diventa un guadagno di comodità reale. La sezione migrare verso Vite o Rspack a fine guida indica i punti di partenza.
I video di questa guida risalgono alla pubblicazione iniziale, nel febbraio 2022. Mostrano il procedimento generale, ma le versioni e il codice aggiornati sono quelli del testo qui sotto.
Le basi: entry, output e server di sviluppo
Webpack legge un file di ingresso, segue gli import uno dopo l’altro e scrive un file di uscita. Tutto il resto è solo un’aggiunta a questo meccanismo.
mkdir -p src dist
npm init --yes
npm install --save-dev webpack webpack-cli webpack-dev-serverLa configurazione minima sta in tre chiavi. entry indica il punto di partenza, output il file prodotto, devServer la cartella servita durante lo sviluppo.
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,
},
};Due punti su cui le vecchie configurazioni sbagliano spesso. output.path deve essere un percorso assoluto, da cui il path.resolve(__dirname, …): un valore come '/dist' indica la radice del disco. E da Webpack 5, devServer.static sostituisce il vecchio contentBase.
Aggiungi gli script npm, poi avvia il server:
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}Sulla macchina di test, npm start risponde con un HTTP 200 su http://localhost:8080/ con 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: cosa è cambiato e perché la tua configurazione non transpila più
È il punto più importante di questo aggiornamento, e quello che rompe in silenzio i progetti vecchi. Babel 8 ha cambiato il proprio target predefinito. Una configurazione che dichiara @babel/preset-env senza indicare un target non transpila più nulla.
npm install --save-dev @babel/core @babel/preset-env babel-loadermodule.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};Ecco la configurazione Babel come la si trova nella maggior parte dei tutorial, compresa la versione 2022 di questa pagina:
{
"presets": ["@babel/preset-env"]
}Con Babel 8.0.1, applicata a questo file sorgente:
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()}`;produce un bundle di 173 byte in cui le arrow function, l’optional chaining ?., il coalescing ?? e il campo privato #valeur sono ancora lì, intatti:
(()=>{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()}`})();In altre parole, Babel viene eseguito, costa tempo di build e non trasforma niente. La correzione sta in una chiave: dichiara il target in modo esplicito.
{
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }]
]
}Con questo target, lo stesso codice produce 1,48 KiB di output ES5, con le funzioni _typeof, _defineProperty e compagnia che ci si aspetta da una transpilazione vera.
C’è una sfumatura da capire, più che un riflesso da applicare: con un target moderno come "defaults", l’output resta identico all’input, ed è normale. Tutti i browser ancora supportati gestiscono nativamente le arrow function, ?., ?? e i campi privati. Se non devi servire browser vecchi, Babel non ha più molto da fare nella tua catena di build. Toglierlo è una decisione legittima, non una scorciatoia.
CSS: caricare fogli di stile dal JavaScript
Due loader lavorano in coppia. css-loader risolve gli @import e gli url() e trasforma il foglio di stile in un modulo JavaScript; style-loader inietta quel modulo in un tag <style> al caricamento della pagina.
npm install --save-dev css-loader style-loadermodule.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};L’ordine conta e si legge da destra a sinistra: css-loader elabora il file, poi style-loader inietta il risultato. Invertili e la build fallisce.
Non resta che importare il foglio di stile dal JavaScript:
import './style.css';Dopo la build, il colore dichiarato in style.css finisce dentro dist/bundle.js: in sviluppo il CSS viaggia dentro il JavaScript. Per la produzione, la sezione successiva lo estrae in un vero file .css.
SASS: compilare lo SCSS e liberarsi di node-sass
Se riprendete un progetto montato prima del 2023, è qui che il build si rompe. L’istruzione storica era npm install --save-dev sass-loader node-sass. Su Node 22 fallisce già all’installazione: node-sass non ha un binario per questa versione di Node e la sua compilazione nativa si ferma, senza che nemmeno sass-loader venga installato:
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.jsE su un progetto in cui node-sass era già presente in node_modules, è il build a rompersi, con un errore netto e un codice di uscita 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 errorSono successe due cose. Il pacchetto node-sass è deprecato — lo annuncia npm stesso, « Node Sass is no longer supported » — e soprattutto sass-loader 17 ha rimosso il supporto per questa implementazione. Non c’è scappatoia: bisogna passare a sass, l’implementazione 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'],
},
],
},
};Nessuna opzione è necessaria: sass-loader rileva sass da solo. La catena si legge sempre da destra a sinistra — SCSS compilato in CSS, poi caricato, poi iniettato.
$couleur: #d62828;
#app {
color: $couleur;
display: flex;
&:hover { text-decoration: underline; }
}Con sass 1.103.1 la build passa e la variabile viene risolta davvero: color:#d62828 compare nell’output.
PostCSS: prefissi e sintassi moderna
A differenza di Babel, PostCSS ha mantenuto tutta la sua utilità. Aggiunge i prefissi dei browser e riporta la sintassi CSS recente a una forma capita dai browser che hai come target.
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 si mette tra css-loader e sass-loader: lo SCSS viene compilato, PostCSS lavora sul CSS ottenuto, poi css-loader lo prende in carico.
Sulla regola della sezione precedente, con user-select e inset aggiunti, l’output mostra il lavoro svolto davvero:
#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}I prefissi vengono aggiunti e inset: 0 viene espanso in quattro proprietà. È un guadagno concreto, al contrario di Babel su un target moderno.
La trappola da conoscere. Molte configurazioni, compresa la versione 2022 di questa pagina, passano l’opzione browsers: 'last 2 versions' a postcss-preset-env. L’opzione è deprecata, e la query stessa è una trappola: in browserslist, last 2 versions significa «le ultime due versioni di ogni browser», Internet Explorer compreso. Verifica fatta con caniuse-lite 1.0.30001810: la query restituisce 30 target, tra cui ie 11, ie 10, ie_mob e op_mini all. Risultato misurato sullo stesso file:
/* con browsers: 'last 2 versions' — IE incluso */
display:-webkit-box; display:-ms-flexbox; display:flex;
/* con il target predefinito (defaults) — 32 target, nessun IE */
display:flex;La buona pratica è togliere l’opzione browsers e dichiarare un campo browserslist nel package.json, letto da PostCSS, da Babel e dal resto della catena:
{
"browserslist": ["defaults", "not dead"]
}Andare in produzione: CSS estratto, HTML generato, hash
Tre aggiunte separano la configurazione di sviluppo da una build consegnabile. mini-css-extract-plugin tira fuori il CSS dal bundle JavaScript e lo mette in un file autonomo. html-webpack-plugin genera l’index.html e ci inietta i nomi di file giusti. [contenthash] e output.clean si occupano della cache e delle pulizie.
npm install --save-dev mini-css-extract-plugin html-webpack-pluginEcco la configurazione completa, quella che è servita per tutte le misurazioni di questa pagina:
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,
},
};Il punto da ricordare: MiniCssExtractPlugin.loader e style-loader si escludono a vicenda. In sviluppo si tiene style-loader, per il ricaricamento a caldo, e in produzione si passa all’estrazione.
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 successfullyL’HTML prodotto fa riferimento ai due file con hash, senza nessun intervento manuale:
<!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’alternativa senza loader: il CSS nativo di Webpack
Webpack sa caricare il CSS senza nessun loader, tramite un’opzione sperimentale. È uno dei cantieri annunciati nella roadmap 2026, destinato a diventare il comportamento predefinito nella versione 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 },
};Senza css-loader, senza style-loader e senza mini-css-extract-plugin, la build produce comunque un file CSS separato:
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 msDa riservare ai progetti che hanno bisogno solo di CSS semplice: l’opzione resta sperimentale e non sostituisce né SASS né PostCSS, che continuano a richiedere i loro loader.
Migrare verso Vite o Rspack: da dove cominciare
Se la diagnosi della prima sezione ti porta verso una migrazione, le strade sono due, e non costano lo stesso.
Rspack è una riscrittura di Webpack in Rust che conserva il modello di configurazione e un’ampia compatibilità con l’API dei plugin. È la strada meno costosa quando la configurazione esistente è pesante: i concetti di entry, di output e delle regole dei loader visti in questa guida restano validi.
Vite obbliga a ripensare la configurazione, ma è il bersaglio predefinito dell’ecosistema. La migrazione si gioca soprattutto sui plugin specifici e sugli import non standard. Le guide francesi citate nelle fonti parlano di una giornata di lavoro su un progetto Vue di media grandezza.
In entrambi i casi il lavoro di questa guida non è sprecato: sapere esattamente cosa fa ogni loader della tua configurazione attuale è proprio ciò che permette di decidere cosa riportare e cosa abbandonare.
Questa guida fa parte delle basi raccolte nell’hub Sviluppo web.
Errori frequenti
sass-loader 17 ha rimosso questa implementazione: la build fallisce su «Unknown Sass implementation "node-sass"» con exit code 1. Sostituiscila con il pacchetto sass.targets o un campo browserslist.ie 11 e ie 10, da cui prefissi -ms-flexbox inutili. L'opzione browsers è comunque deprecata.style-loader in sviluppo e passa all'estrazione in produzione.'/dist' indica la radice del disco. Usa path.resolve(__dirname, 'dist').['style-loader', 'css-loader'] esegue prima css-loader. Invertito, la build fallisce.