Breve historia: de los scripts globales a Vite
De los scripts globales a CommonJS, AMD y ESM; de Grunt y Gulp a webpack, y de webpack a esbuild y Vite. Cada salto resolvió un dolor concreto que el anterior no podía. La genealogía importa más que los logos.
La historia de las herramientas de build no es una sucesión de modas, sino una cadena de respuestas a dolores concretos. Cada salto —de los scripts globales a los módulos, de las task runners a los bundlers, de los bundlers a los dev servers— resolvió un problema real que el eslabón anterior no podía. Entender por qué apareció cada pieza vale mucho más que memorizar sus nombres, porque los nombres cambian y los problemas se repiten.
- Trazar el camino de los scripts globales a CommonJS, AMD y ESM.
- Entender por qué Node necesitó módulos y el navegador no podía copiarlos.
- Distinguir una task runner de un bundler y por qué ganó el bundler.
- Ver qué dolor concreto motivó cada salto hasta llegar a Vite.
Del ámbito global a los módulos
El punto de partida fue el caos que ya conoces: muchos <script> colgando del objeto global, orden frágil y colisiones silenciosas. La primera defensa seria llegó del servidor. En 2009, Node adoptó CommonJS: un sistema de módulos con require y module.exports, síncrono porque en un servidor los archivos están en el disco local y leerlos es instantáneo. Sobre CommonJS creció npm, y con él la idea de que el código se distribuye en paquetes.
// CommonJS (Node, 2009): sincrono, require y module.exports
const fs = require("fs");
module.exports = function leer(ruta) {
return fs.readFileSync(ruta, "utf8");
};
El navegador no podía copiar ese modelo: un require síncrono que fuera a buscar un archivo por la red bloquearía la interfaz. Lo que en el servidor era instantáneo, en el cliente habría congelado la página en cada importación.
La respuesta fue AMD (Asynchronous Module Definition) y su implementación estrella, RequireJS, que declaraba las dependencias por adelantado y las cargaba de forma asíncrona con una función define. El precio era una sintaxis envolvente y verbosa.
// AMD (RequireJS): declara las dependencias antes de usarlas y las carga sin bloquear
define(["jquery"], function ($) {
return function pintar() {
$("body").append("<p>hola</p>");
};
});
Para sobrevivir en ambos mundos apareció UMD, un patrón envolvente que detectaba el entorno y se adaptaba a CommonJS o a AMD. Tres sistemas resolvieron el mismo problema con supuestos distintos, y esos supuestos explican por qué convivieron tanto tiempo:
- CommonJS — síncrono y dinámico:
requirese resuelve al ejecutar. Ideal en disco, inviable por red sin bloquear. - AMD — asíncrono y dinámico: declara las dependencias por adelantado para cargarlas sin congelar el navegador.
- ESM — asíncrono y estático: las importaciones se conocen antes de ejecutar, y esa estaticidad es la que después habilita el tree shaking.
El desenlace llegó en 2015, cuando ECMAScript estandarizó ESM con import y export. Por fin un único sistema, estático y analizable, válido tanto para Node como para el navegador. Tardó años en tener soporte completo en ambos, pero unificó una década de fragmentación.
// ESM (estandar, 2015): estatico, import y export
import { readFileSync } from "node:fs";
export function leer(ruta) {
return readFileSync(ruta, "utf8");
}
Que ESM sea estático —que sus importaciones se puedan leer sin ejecutar el código— parece un matiz académico, pero es la propiedad de la que cuelga casi todo lo demás. Solo porque un bundler puede analizar el grafo sin correrlo es capaz de eliminar exports muertos, dividir en chunks y reordenar. CommonJS, al ser dinámico, no ofrecía esa garantía. Cada vez que veas la palabra “estático” en este track, tradúcela como “analizable, y por tanto optimizable”.
De las task runners a los bundlers
Con los módulos resueltos en teoría, faltaba una herramienta que preparase el código para producción. La primera generación fueron task runners. Grunt (2012) orquestaba tareas —concatenar, minificar, comprobar estilo— a partir de un gran objeto de configuración: mucha ceremonia declarativa. Gulp (2013) le respondió con código sobre configuración y con flujos encadenados mediante pipe, más rápidos y legibles.
// Gulp: codigo sobre configuracion, transformaciones encadenadas con pipe
gulp.task("scripts", () =>
gulp.src("src/*.js").pipe(minificar()).pipe(gulp.dest("dist"))
);
Pero ambos seguían siendo capataces que ejecutaban transformaciones sobre archivos, sin comprender de verdad las dependencias entre ellos. El giro conceptual lo trajo Browserify (2011): en lugar de orquestar tareas, seguía el grafo de require desde un punto de entrada y producía un único archivo que el navegador podía cargar. Trajo CommonJS al navegador empaquetándolo. Esa idea —modelar la aplicación como un grafo de dependencias— la llevó a su máxima expresión webpack (2014).
webpack: todo es un módulo
No solo JavaScript: con loaders, también CSS, imágenes o fuentes entraban al grafo. Trajo code splitting, carga diferida y HMR. Reinó una década, a cambio de una configuración compleja.
Rollup: primero ESM
Nació orientado a ESM y popularizó el tree shaking. Su salida limpia lo convirtió en la opción de referencia para empaquetar librerías, no aplicaciones.
Parcel: cero configuración
Fue la reacción directa a la complejidad de webpack: funcionaba sin apenas config. Demostró que la ergonomía era una demanda real, no un lujo.
El grafo como modelo
El bundler ganó porque describía la aplicación tal como es —un grafo de dependencias—, mientras la task runner solo veía una lista de archivos sueltos.
La revolución de la velocidad y el dev server
Hacia 2019 el modelo de webpack empezó a crujir. A medida que las aplicaciones crecían, empaquetarlo todo antes de servir se volvía caro. El problema ya no era la corrección, sino la velocidad y la experiencia de desarrollo. El diagnóstico de aquel momento se resume en tres síntomas:
- El arranque en frío tardaba decenas de segundos, porque había que empaquetar todo antes de mostrar nada.
- El HMR se degradaba a medida que el grafo crecía, justo cuando el proyecto más lo necesitaba.
- Cambiar una sola línea obligaba a reconstruir artefactos enormes.
Aparecieron dos ideas complementarias. Snowpack propuso no empaquetar en desarrollo: servir ESM nativo y transformar cada archivo por separado, delegando en el propio cargador de módulos del navegador. Y esbuild (2020), escrito en Go, hizo que transformar y empaquetar fueran entre diez y cien veces más rápidos que las herramientas escritas en JavaScript. La síntesis de ambas fue Vite (2021).
Snowpack
La chispa conceptual: en desarrollo, no empaquetar. Servir ESM nativo y transformar cada archivo por separado.
esbuild
Escrito en Go, hizo que transpilar y empaquetar dejaran de ser el cuello de botella. Otras herramientas lo adoptaron como dependencia interna.
Vite en desarrollo
Arranque casi instantáneo sirviendo ESM, con esbuild pre-empaquetando solo las dependencias que sí conviene fundir.
Vite en producción
Para producción sí empaqueta, con Rollup y su tree shaking. Dos motores, uno por cada tiempo: la plantilla que definió la era.
flowchart TD A[2009 CommonJS y Node] --> B[2011 AMD y RequireJS] B --> C[2012 Grunt y Browserify] C --> D[2013 Gulp] D --> E[2014 webpack] E --> F[2015 ESM estandar] F --> G[2015 Rollup y tree shaking] G --> H[2020 esbuild y Snowpack] H --> I[2021 Vite] I --> J[2026 Rolldown y Oxc] style A fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style I fill:#a6e3a1,color:#11111b style J fill:#cba6f7,color:#11111b
La tentación al estudiar esta historia es coleccionar nombres: saber que webpack vino antes que Vite como quien memoriza reyes. Es la lectura inútil. La lectura valiosa es que cada herramienta codifica las restricciones de su época. CommonJS asumía disco local y por eso podía ser síncrono; AMD asumía la red y por eso tuvo que ser asíncrono; webpack asumía HTTP/1 y navegadores sin módulos, y por eso lo empaquetaba todo; Vite asume ESM nativo y HTTP/2, y por eso puede no empaquetar en desarrollo. Cuando entiendes que cada diseño es la sombra de un cuello de botella —la red de su momento, las capacidades del navegador de su momento, el tamaño de las aplicaciones de su momento—, dejas de preguntar cuál herramienta es mejor en abstracto y empiezas a preguntar qué restricción resolvía y si esa restricción sigue vigente. Esa mirada te vuelve inmune a la moda: cuando aparezca la próxima herramienta, no la juzgarás por su ruido, sino por el dolor concreto que dice resolver. Y sabrás que el patrón se repetirá, porque el problema de fondo —acercar lo que escribimos a lo que se ejecuta— no se ha ido a ninguna parte.
- Abre un
webpack.config.jsde algún proyecto antiguo e identifica qué loaders y plugins resolvían problemas que hoy Vite da por hechos. - Compáralo con un
vite.config.tsmoderno y anota cuánta configuración ha desaparecido y por qué. - Busca un paquete en npm que aún publique en formato UMD y razona contra qué entornos se estaba protegiendo.
- Ordena en una línea de tiempo propia los cinco saltos y escribe, junto a cada uno, la única frase del dolor que lo motivó.