Tutorial · JavaScript

Tutorial de Webpack 5: configurar un build existente en 2026

11 min · Principiante · CSS, JavaScript · verificado el 7 septiembre 2026

Respuesta rápida

En una configuración Webpack 5 que retomas hoy, dos cosas rompen en silencio: node-sass, que sass-loader 17 ya no reconoce en absoluto, y @babel/preset-env sin target declarado, que desde Babel 8 ya no transpila nada. Esta guía da la configuración completa, base, Babel, CSS, SASS, PostCSS y producción, reejecutada el 2 de septiembre de 2026 con Webpack 5.110.3 y Node 22.

Esta guía parte de una constatación sencilla: en 2026 rara vez abres un proyecto Webpack para crearlo, lo abres para repararlo. Las cuatro piezas que dan problemas son siempre las mismas: transpilar el JavaScript, cargar el CSS, compilar SCSS y aplicar PostCSS. Aquí están reunidas en una sola configuración probada con Webpack 5.110.3 y Node 22.

¿Sigue teniendo sentido usar Webpack en 2026?

La pregunta merece una respuesta franca antes de configurar nada, porque decide el esfuerzo que vas a invertir.

Para un proyecto nuevo, no. Vite es la opción por defecto de Vue y de Laravel, y también hacia donde apunta la documentación de la mayoría de frameworks front. Arrancar un proyecto con Webpack en 2026 equivale a escribir a mano una configuración que las demás herramientas ya traen hecha.

Para un proyecto existente, sí, y sin remordimientos. Webpack no está abandonado: la versión 5.110.3 es la que se ha usado para esta guía, la hoja de ruta 2026 anuncia el CSS nativo, la transpilación TypeScript integrada y los puntos de entrada HTML, y la versión 6 está prevista para finales de 2027. Una base de código con una configuración pesada, loaders propios o plugins específicos no tiene ningún motivo para migrar con prisas.

La regla práctica: si tu arranque en frío se queda por debajo de cinco segundos, el tema no es prioritario. Si pasa de treinta segundos, migrar se convierte en una ganancia real de comodidad. La sección migrar a Vite o Rspack, al final de la guía, indica por dónde empezar.

Los vídeos de esta guía son de la publicación inicial, en febrero de 2022. Muestran el planteamiento general, pero las versiones y el código al día son los del texto que sigue.

La base: entrada, salida y servidor de desarrollo

Webpack lee un fichero de entrada, sigue los import uno tras otro y escribe un fichero de salida. Todo lo demás no es más que un añadido a ese mecanismo.

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

La configuración mínima cabe en tres claves. entry indica el punto de partida, output el fichero producido y devServer la carpeta servida durante el desarrollo.

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

Dos puntos en los que las configuraciones antiguas se equivocan a menudo. output.path tiene que ser una ruta absoluta, de ahí el path.resolve(__dirname, …): un valor como ’/dist’ apunta a la raíz del disco. Y desde Webpack 5, devServer.static sustituye al antiguo contentBase.

Añade los scripts npm y arranca el servidor:

package.json
{
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}

En la máquina de pruebas, npm start responde con un HTTP 200 en http://localhost:8080/ con 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: qué ha cambiado y por qué tu configuración ya no transpila

Es el punto más importante de esta actualización, y el que rompe en silencio los proyectos antiguos. Babel 8 ha cambiado su target por defecto. Una configuración que declara @babel/preset-env sin indicar target ya no transpila nada.

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

Esta es la configuración de Babel tal y como aparece en la mayoría de tutoriales, incluida la versión de 2022 de esta página:

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

Con Babel 8.0.1, aplicada a este fichero fuente:

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

produce un bundle de 173 bytes en el que las funciones flecha, el encadenamiento opcional ?., la coalescencia ?? y el campo privado #valeur siguen ahí, intactos:

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

Dicho de otro modo: Babel se ejecuta, cuesta tiempo de build y no transforma nada. El arreglo cabe en una clave: declara el target de forma explícita.

.babelrc
{
  "presets": [
    ["@babel/preset-env", { "targets": { "ie": "11" } }]
  ]
}

Con ese target, el mismo código produce 1,48 KiB de salida ES5, con las funciones _typeof, _defineProperty y compañía que se esperan de una transpilación de verdad.

Hay un matiz que conviene entender en lugar de aplicar un reflejo: con un target moderno como "defaults", la salida es idéntica a la entrada, y es normal. Todos los navegadores con soporte vigente admiten de forma nativa las funciones flecha, ?., ?? y los campos privados. Si no tienes que dar servicio a navegadores antiguos, a Babel ya no le queda gran cosa que hacer en tu cadena de build. Quitarlo es una decisión legítima, no un atajo.

CSS: cargar hojas de estilo desde JavaScript

Dos loaders trabajan en pareja. css-loader resuelve los @import y los url() y convierte la hoja en un módulo JavaScript; style-loader inyecta ese módulo en una etiqueta <style> al cargar la página.

bash
npm install --save-dev css-loader style-loader
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

El orden importa y se lee de derecha a izquierda: css-loader procesa el fichero y luego style-loader inyecta el resultado. Si los inviertes, el build falla.

Solo queda importar la hoja desde el JavaScript:

src/index.js
import './style.css';

Tras el build, el color declarado en style.css acaba dentro de dist/bundle.js: en desarrollo, el CSS viaja dentro del JavaScript. Para producción, la sección siguiente lo extrae a un fichero .css de verdad.

SASS: compilar SCSS y deshacerse de node-sass

Si retoma un proyecto montado antes de 2023, aquí es donde el build se rompe. La instrucción histórica era npm install --save-dev sass-loader node-sass. En Node 22 falla ya en la instalación: node-sass no tiene binario para esa versión de Node y su compilación nativa se detiene, sin que sass-loader se instale tampoco:

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

Y en un proyecto donde node-sass ya estaba presente en node_modules, es el build el que se rompe, con un error claro y un código de salida 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

Han ocurrido dos cosas. El paquete node-sass está obsoleto — npm lo anuncia él mismo, « Node Sass is no longer supported » — y sobre todo sass-loader 17 ha retirado la compatibilidad con esa implementación. No hay atajo: hay que pasar a sass, la implementación en 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'],
      },
    ],
  },
};

No hace falta ninguna opción: sass-loader detecta sass automáticamente. La cadena se sigue leyendo de derecha a izquierda: el SCSS se compila a CSS, luego se carga, luego se inyecta.

src/style.scss
$couleur: #d62828;

#app {
  color: $couleur;
  display: flex;

  &:hover { text-decoration: underline; }
}

Con sass 1.103.1 el build pasa y la variable se resuelve bien: color:#d62828 aparece en la salida.

PostCSS: prefijos y sintaxis moderna

A diferencia de Babel, PostCSS conserva toda su utilidad. Añade los prefijos de los fabricantes y rebaja la sintaxis CSS reciente a una forma que entienden tus navegadores objetivo.

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 se coloca entre css-loader y sass-loader: el SCSS se compila, PostCSS trabaja sobre el CSS resultante y después css-loader se hace cargo.

Sobre la regla de la sección anterior, con user-select e inset añadidos, la salida muestra el trabajo realmente hecho:

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}

Los prefijos se añaden e inset: 0 se despliega en cuatro propiedades. Es una ganancia concreta, al contrario que Babel con un target moderno.

La trampa que hay que conocer. Muchas configuraciones, incluida la versión de 2022 de esta página, pasan la opción browsers: ’last 2 versions’ a postcss-preset-env. Esa opción está obsoleta, y la consulta en sí es una trampa: en browserslist, last 2 versions significa «las dos últimas versiones de cada navegador», Internet Explorer incluido. Comprobado con caniuse-lite 1.0.30001810, esa consulta devuelve 30 targets, entre ellos ie 11, ie 10, ie_mob y op_mini all. Resultado medido sobre el mismo fichero:

css
/* con browsers: 'last 2 versions' — IE incluido */
display:-webkit-box; display:-ms-flexbox; display:flex;

/* con el target por defecto (defaults) — 32 targets, ningún IE */
display:flex;

La buena práctica es eliminar la opción browsers y declarar un campo browserslist en el package.json, que leen PostCSS, Babel y el resto de la cadena:

package.json
{
  "browserslist": ["defaults", "not dead"]
}

Pasar a producción: extracción del CSS, HTML generado y hash

Tres añadidos separan la configuración de desarrollo de un build entregable. mini-css-extract-plugin saca el CSS del bundle JavaScript a un fichero independiente. html-webpack-plugin genera el index.html e inyecta en él los nombres de fichero correctos. El [contenthash] y output.clean se encargan de la caché y de la limpieza.

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

Esta es la configuración completa, la que ha servido para todas las mediciones de esta página:

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

Lo que hay que retener: MiniCssExtractPlugin.loader y style-loader se excluyen mutuamente. Se mantiene style-loader en desarrollo, para la recarga en caliente, y se pasa a la extracción en producción.

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

El HTML producido referencia los dos ficheros con hash, sin intervención manual:

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>

La alternativa sin loader: el CSS nativo de Webpack

Webpack sabe cargar CSS sin ningún loader, mediante una opción experimental. Es uno de los frentes anunciados en la hoja de ruta 2026, destinado a convertirse en el comportamiento por defecto en la versión 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 },
};

Sin css-loader, sin style-loader y sin mini-css-extract-plugin, el build produce efectivamente un fichero CSS aparte:

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

Resérvalo para proyectos que solo necesiten CSS sencillo: la opción sigue siendo experimental y no sustituye ni a SASS ni a PostCSS, que siguen exigiendo sus loaders.

Migrar a Vite o Rspack: por dónde empezar

Si el diagnóstico de la primera sección te lleva hacia una migración, hay dos caminos, y no cuestan lo mismo.

Rspack es una reescritura de Webpack en Rust que conserva el modelo de configuración y una amplia compatibilidad con la API de plugins. Es el camino más barato cuando la configuración existente es pesada: los conceptos de entry, de output y de reglas de loaders vistos en esta guía siguen siendo válidos.

Vite obliga a replantear la configuración, pero es el objetivo por defecto del ecosistema. La migración se juega sobre todo en los plugins específicos y en los imports no estándar. Las guías francesas citadas en las fuentes hablan de una jornada de trabajo en un proyecto Vue de tamaño medio.

En ambos casos, el trabajo de esta guía no se pierde: saber exactamente qué hace cada loader de tu configuración actual es justo lo que permite decidir qué merece la pena llevarse y qué hay que abandonar.

Esta guía forma parte de los fundamentos reunidos en el hub Desarrollo web.

Errores frecuentes

node-sass detiene el build sass-loader 17 ha retirado esta implementación: el build falla con «Unknown Sass implementation "node-sass"» y un código de salida 1. Sustitúyela por el paquete sass.
@babel/preset-env sin targets ya no transpila Desde Babel 8 el target por defecto ha cambiado: el código moderno sale intacto. Declara targets o un campo browserslist.
browsers: 'last 2 versions' hace entrar a Internet Explorer En browserslist, esa consulta devuelve 30 targets, entre ellos ie 11 e ie 10, de ahí prefijos -ms-flexbox inútiles. La opción browsers está obsoleta de todas formas.
style-loader y MiniCssExtractPlugin en la misma regla Se excluyen mutuamente. Mantén style-loader en desarrollo y pasa a la extracción en producción.
output.path tiene que ser absoluto Un valor como '/dist' apunta a la raíz del disco. Usa path.resolve(__dirname, 'dist').
El orden de los loaders se lee de derecha a izquierda ['style-loader', 'css-loader'] ejecuta primero css-loader. Al revés, el build falla.
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.