Tutorial Webpack 5: configurar um build (Babel, CSS, SASS, PostCSS)
11 min · Iniciante · CSS, JavaScript · verificado a 7 Setembro 2026
Numa configuração Webpack 5 retomada hoje, há duas coisas que partem sem aviso: o node-sass, que o sass-loader 17 já não reconhece de todo, e o @babel/preset-env sem alvo declarado, que desde o Babel 8 deixou de transpilar. Este guia dá a configuração completa, base, Babel, CSS, SASS, PostCSS, produção, reexecutada a 2 de setembro de 2026 no Webpack 5.110.3 e no Node 22.
Este guia parte de uma constatação simples: em 2026, raramente se abre um projeto Webpack para o criar — abre-se para o reparar. As quatro peças que dão problemas são sempre as mesmas: transpilar o JavaScript, carregar o CSS, compilar SCSS, aplicar PostCSS. Estão todas reunidas aqui, numa única configuração testada no Webpack 5.110.3 e no Node 22.
Ainda vale a pena usar o Webpack em 2026?
A pergunta merece uma resposta franca antes de configurares seja o que for, porque é ela que decide o esforço que vais investir.
Para um projeto novo, não. O Vite é a escolha por omissão do Vue e do Laravel, e é também para ele que aponta a documentação da maior parte das frameworks front-end. Arrancar um projeto com Webpack em 2026 é escrever à mão uma configuração que as outras ferramentas já entregam feita.
Para um projeto existente, sim, e sem culpa. O Webpack não está abandonado: a versão 5.110.3 é a que serviu para este guia, o roadmap de 2026 anuncia o CSS nativo, a transpilação de TypeScript integrada e os pontos de entrada em HTML, e a versão 6 está anunciada para o final de 2027. Uma base de código com uma configuração pesada, loaders caseiros ou plugins específicos não tem motivo nenhum para migrar à pressa.
A regra prática: se o teu arranque a frio fica abaixo dos cinco segundos, o assunto não é prioritário. Se passar dos trinta segundos, a migração torna-se um ganho de conforto real. A secção migrar para o Vite ou o Rspack, no fim do guia, dá os pontos de partida.
Os vídeos deste guia são da publicação inicial, em fevereiro de 2022. Mostram a abordagem geral, mas as versões e o código atualizados são os do texto abaixo.
A base: entrada, saída e servidor de desenvolvimento
O Webpack lê um ficheiro de entrada, segue os import de ficheiro em ficheiro e escreve um ficheiro de saída. Todo o resto é acrescento a este mecanismo.
mkdir -p src dist
npm init --yes
npm install --save-dev webpack webpack-cli webpack-dev-serverA configuração mínima resume-se a três chaves. entry indica o ponto de partida, output o ficheiro produzido e devServer a pasta servida durante o desenvolvimento.
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,
},
};Dois pontos em que as configurações antigas erram muitas vezes. output.path tem de ser um caminho absoluto, daí o path.resolve(__dirname, …): um valor como '/dist' aponta para a raiz do disco. E, desde o Webpack 5, devServer.static substitui o antigo contentBase.
Acrescenta os scripts npm e arranca o servidor:
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}Na máquina de teste, o npm start responde em HTTP 200 em http://localhost:8080/ com o 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: o que mudou, e porque é que a tua configuração já não transpila
É o ponto mais importante desta atualização e o que parte os projetos antigos sem dizer nada. O Babel 8 mudou o seu alvo por omissão. Uma configuração que declara @babel/preset-env sem indicar um alvo deixou de transpilar seja o que for.
npm install --save-dev @babel/core @babel/preset-env babel-loadermodule.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};Esta é a configuração do Babel tal como aparece na maioria dos tutoriais, incluindo a versão de 2022 desta página:
{
"presets": ["@babel/preset-env"]
}Com o Babel 8.0.1, aplicada a este ficheiro de origem:
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()}`;ela produz um bundle de 173 bytes onde as arrow functions, o optional chaining ?., o operador ?? e o campo privado #valeur continuam lá, intactos:
(()=>{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()}`})();Por outras palavras: o Babel corre, custa tempo de build e não transforma nada. A correção resume-se a uma chave — declara o alvo explicitamente.
{
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }]
]
}Com esse alvo, o mesmo código produz 1,48 KiB de saída ES5, com as funções _typeof, _defineProperty e companhia que se esperam de uma transpilação a sério.
Há aqui uma nuance para perceber, não um reflexo para aplicar: com um alvo moderno como "defaults", a saída fica igual à entrada, e isso é normal. Todos os browsers ainda acompanhados tratam nativamente as arrow functions, o ?., o ?? e os campos privados. Se não tens browsers antigos para servir, o Babel já não tem grande coisa a fazer na tua cadeia de build. Retirá-lo é uma decisão legítima, não um atalho.
CSS: carregar folhas de estilo a partir do JavaScript
Dois loaders trabalham em par. O css-loader resolve os @import e os url() e transforma a folha de estilo num módulo JavaScript; o style-loader injeta esse módulo numa tag <style> quando a página carrega.
npm install --save-dev css-loader style-loadermodule.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};A ordem conta e lê-se da direita para a esquerda: o css-loader trata o ficheiro, depois o style-loader injeta o resultado. Inverte os dois e o build falha.
Falta apenas importar a folha de estilo a partir do JavaScript:
import './style.css';Depois do build, a cor declarada em style.css aparece mesmo dentro de dist/bundle.js: em desenvolvimento, o CSS viaja dentro do JavaScript. Para produção, a secção seguinte extrai-o para um verdadeiro ficheiro .css.
SASS: compilar SCSS e livrar-se do node-sass
Se retomar um projeto montado antes de 2023, é aqui que o build vai partir. A instrução histórica era npm install --save-dev sass-loader node-sass. No Node 22 falha logo na instalação: o node-sass não tem binário para esta versão do Node e a sua compilação nativa para, sem que o sass-loader seja instalado também:
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 num projeto em que o node-sass já estava presente em node_modules, é o build que parte, com um erro claro e um código de saída 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 errorAconteceram duas coisas. O pacote node-sass está obsoleto — o próprio npm o anuncia, « Node Sass is no longer supported » — e sobretudo o sass-loader 17 retirou o suporte a esta implementação. Não há contorno: é preciso passar a sass, a implementação em 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'],
},
],
},
};Não é preciso opção nenhuma: o sass-loader deteta o sass automaticamente. A cadeia continua a ler-se da direita para a esquerda — SCSS compilado em CSS, depois carregado, depois injetado.
$couleur: #d62828;
#app {
color: $couleur;
display: flex;
&:hover { text-decoration: underline; }
}Com o sass 1.103.1, o build passa e a variável fica bem resolvida: color:#d62828 aparece na saída.
PostCSS: prefixos e sintaxe moderna
Ao contrário do Babel, o PostCSS manteve toda a sua utilidade. Acrescenta os prefixos dos fabricantes e baixa a sintaxe CSS recente para uma forma que os teus browsers-alvo compreendem.
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 })],
};O postcss-loader fica entre o css-loader e o sass-loader: o SCSS é compilado, o PostCSS trabalha sobre o CSS obtido, depois o css-loader toma conta dele.
Sobre a regra da secção anterior, com user-select e inset acrescentados, a saída mostra o trabalho realmente feito:
#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}Os prefixos são acrescentados e o inset: 0 é desdobrado em quatro propriedades. É um ganho concreto, ao contrário do Babel num alvo moderno.
A armadilha a conhecer. Muitas configurações, incluindo a versão de 2022 desta página, passam a opção browsers: 'last 2 versions' ao postcss-preset-env. Essa opção está descontinuada e a própria query é uma armadilha: no browserslist, last 2 versions significa «as duas últimas versões de cada browser», Internet Explorer incluído. Verificação feita com o caniuse-lite 1.0.30001810: esta query devolve 30 alvos, entre eles ie 11, ie 10, ie_mob e op_mini all. Resultado medido no mesmo ficheiro:
/* com browsers: 'last 2 versions' — IE incluído */
display:-webkit-box; display:-ms-flexbox; display:flex;
/* com o alvo predefinido (defaults) — 32 alvos, nenhum IE */
display:flex;A boa prática é remover a opção browsers e declarar um campo browserslist no package.json, lido pelo PostCSS, pelo Babel e pelo resto da cadeia:
{
"browserslist": ["defaults", "not dead"]
}Passar a produção: extração do CSS, HTML gerado, hash
Três acrescentos separam a configuração de desenvolvimento de um build entregável. O mini-css-extract-plugin tira o CSS do bundle JavaScript para um ficheiro autónomo. O html-webpack-plugin gera o index.html e injeta-lhe os nomes de ficheiro certos. O [contenthash] e o output.clean tratam da cache e da arrumação.
npm install --save-dev mini-css-extract-plugin html-webpack-pluginAqui fica a configuração completa, a que serviu para todas as medições desta página:
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,
},
};O ponto a reter: MiniCssExtractPlugin.loader e style-loader excluem-se mutuamente. Mantém-se o style-loader em desenvolvimento, para o hot reload, e passa-se para a extração em produção.
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 successfullyO HTML produzido referencia os dois ficheiros com hash, sem intervenção manual:
<!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>A alternativa sem loader: o CSS nativo do Webpack
O Webpack sabe carregar CSS sem loader nenhum, através de uma opção experimental. É uma das frentes anunciadas no roadmap de 2026, destinada a tornar-se o comportamento por omissão na versão 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 },
};Sem css-loader, sem style-loader e sem mini-css-extract-plugin, o build produz mesmo um ficheiro CSS separado:
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 msA reservar para projetos que só precisam de CSS simples: a opção continua experimental e não substitui nem o SASS nem o PostCSS, que continuam a exigir os seus loaders.
Migrar para o Vite ou o Rspack: por onde começar
Se o diagnóstico da primeira secção te empurra para uma migração, há dois caminhos, e não custam o mesmo.
O Rspack é uma reescrita do Webpack em Rust que mantém o modelo de configuração e uma boa compatibilidade com a API dos plugins. É o caminho mais barato quando a configuração existente é pesada: os conceitos de entry, de output e das regras de loaders vistos neste guia continuam válidos.
O Vite obriga a repensar a configuração, mas é o alvo por omissão do ecossistema. A migração joga-se sobretudo nos plugins específicos e nos imports fora do padrão. Os guias franceses citados nas fontes falam de um dia de trabalho num projeto Vue de dimensão média.
Nos dois casos, o trabalho deste guia não se perde: saber ao certo o que faz cada loader da tua configuração atual é exatamente o que permite decidir o que levar e o que abandonar.
Este guia faz parte das bases reunidas no hub Desenvolvimento web.
Erros frequentes
sass-loader 17 retirou esta implementação: o build falha com «Unknown Sass implementation "node-sass"» e um código de saída 1. Substitui-a pelo pacote sass.targets ou um campo browserslist.ie 11 e ie 10, daí prefixos -ms-flexbox inúteis. A opção browsers está de qualquer forma descontinuada.style-loader em desenvolvimento e passa para a extração em produção.'/dist' aponta para a raiz do disco. Usa path.resolve(__dirname, 'dist').['style-loader', 'css-loader'] executa primeiro o css-loader. Ao contrário, o build falha.