wandres.dev
ESBUILD, SWC Y OXC · el toolchain en Rust/Go

esbuild: bundler y transformador en un solo binario

esbuild reunió en un programa escrito en Go dos oficios que antes vivían separados: empaquetar y transpilar. Entre 50 y 100 veces más rápido que Babel, redefinió qué velocidad era posible y sostuvo durante años la mitad de desarrollo de Vite.

⏱ 15 min

esbuild no fue solo una herramienta más rápida; fue la prueba de que casi todo el tooling de JavaScript pagaba un peaje evitable. Nacido del trabajo de un único ingeniero y escrito en Go, hacía a la vez dos trabajos que la generación anterior repartía entre Babel, Webpack y Terser: transpilaba y empaquetaba, en una sola pasada y a una velocidad que parecía un error de medición. Esta lección disecciona qué es esbuild, por qué su elección de lenguaje pesa tanto como su diseño, y por qué durante años fue el corazón silencioso del modo de desarrollo de Vite.

🎯 Al terminar esta lección sabrás
  • Situar a esbuild como bundler y transformador a la vez, no como una cosa u otra.
  • Explicar por qué escribirlo en Go, y no en JavaScript, cambia el orden de magnitud.
  • Reconstruir su papel histórico en Vite: pre-empaquetar dependencias y transpilar en dev.
  • Delimitar sus límites como bundler de producción y entender su legado.

Un binario, dos oficios

esbuild borra una frontera que parecía natural. En el stack clásico, transpilar y empaquetar eran tareas de herramientas distintas: Babel bajaba TypeScript y JSX a un JavaScript que el motor entendía, y después Webpack o Rollup recorrían el grafo de módulos para coserlos en un bundle, casi siempre con Terser minificando al final. esbuild colapsa esa cadena en un único programa que parsea, transforma, resuelve, empaqueta y minifica sin salir jamás de su propio proceso.

Que sea las dos cosas a la vez no es una etiqueta comercial, sino una decisión de arquitectura. Al vivir bajo un mismo techo, el parser produce un AST que el transformador y el bundler reutilizan sin serializarlo, sin releer el texto y sin cruzar la frontera entre procesos que la cadena clásica cruzaba una y otra vez. El trabajo no se reparte entre programas que se intercambian cadenas de texto: se hace una vez, sobre una sola representación en memoria.

# esbuild como transformador: transpila un archivo, sin empaquetar
esbuild app.tsx --loader=tsx > app.js

# esbuild como bundler: recorre el grafo y produce un solo artefacto
esbuild app.tsx --bundle --minify --outfile=out.js

El mismo binario, dos invocaciones. En la primera es un sustituto de Babel; en la segunda, un sustituto de la tríada Babel más Webpack más Terser. Esa doble naturaleza es justo lo que lo distingue de las piezas que verás en las lecciones siguientes.

Merece la pena detenerse en por qué esa unión pesa tanto. La cadena clásica no era lenta solo por estar en JavaScript, sino por su topología: cada herramienta era un proceso independiente que recibía texto, lo parseaba a su propio árbol, hacía su trabajo y volvía a emitir texto para el siguiente eslabón. esbuild sustituye esa procesión de traducciones por un único recorrido en memoria, y absorbe de un golpe tres oficios que antes eran productos separados:

  • El papel de Babel: parsear, borrar los tipos de TypeScript y convertir el JSX.
  • El papel de Webpack o Rollup: resolver el grafo de módulos y coserlo en un artefacto.
  • El papel de Terser: minificar la salida para producción.

Que los tres compartan proceso, parser y árbol es lo que convierte tres pasadas en una, y buena parte de su ventaja nace de ahí antes incluso de hablar del lenguaje.

Esa unión tiene un límite que conviene anticipar: hacer todo en una sola herramienta simplifica el camino común pero reduce los puntos donde intervenir. La cadena clásica, con sus procesos separados, ofrecía mil ganchos para plugins entre etapa y etapa; esbuild, al fundirlas, expone menos superficie de extensión. Es el clásico intercambio entre integración y extensibilidad, y explica buena parte del papel que acabaría ocupando.

Por qué Go y no JavaScript

La velocidad de esbuild no es magia, sino la suma de decisiones que las herramientas en JavaScript no podían tomar. Go compila a código máquina nativo, así que no paga el peaje de interpretarse a sí mismo mientras procesa tu código. Sus goroutines reparten el parseo y la transformación entre todos los núcleos disponibles, cuando las herramientas clásicas corrían en un solo hilo. Controla la disposición de la memoria para ser amable con la caché de la CPU, y evita las pausas de recolección de basura que castigan a un proceso largo. Y, por encima de todo, está diseñado para parsear el código una sola vez y reutilizar ese árbol en cada fase.

// Entrada: un modulo con tipos y JSX
export const Saludo = ({ nombre }: { nombre: string }) =>
  jsx("h1", { children: ["Hola ", nombre] });
// Salida: tipos borrados y sintaxis rebajada al target
export const Saludo = ({ nombre }) =>
  jsx("h1", { children: ["Hola ", nombre] });

De esa combinación sale la cifra que hizo famoso al proyecto: como transformador, esbuild corre entre 50 y 100 veces más rápido que Babel, y como bundler llega a ser hasta cien veces más veloz que las cadenas equivalentes en JavaScript. Borrar los tipos de un .ts o convertir el JSX de un .tsx deja de tener coste apreciable.

El modelo de concurrencia explica buena parte de esa distancia. JavaScript corre sobre un solo hilo y un bucle de eventos: por muchos núcleos que tenga tu máquina, un transpilador clásico usa uno. Go nació con la concurrencia en el centro del lenguaje, y esbuild la aprovecha para parsear y transformar archivos en paralelo, saturando la CPU en lugar de desperdiciarla. Sumado a que no reparsea entre fases —el archivo se lee una vez y el árbol se reutiliza—, el resultado no es algo más rápido: es rápido en otro orden de magnitud.

El corazón del dev server de Vite

Durante toda su primera etapa —de Vite 2 a Vite 6— Vite montó dos motores, y esbuild era el de la mitad de desarrollo. Allí hacía dos trabajos precisos. El primero, el pre-bundling de dependencias vía optimizeDeps: escaneaba lo que importabas de node_modules, convertía los paquetes en CommonJS a ESM y colapsaba los cientos de archivos internos de una librería en unos pocos módulos servibles. El segundo, transpilar bajo demanda: cada vez que el navegador pedía un .ts o un .tsx, esbuild le borraba los tipos y convertía el JSX al vuelo, en milisegundos.

# La huella de esbuild en un proyecto Vite clasico
node_modules/.vite/deps/    # dependencias pre-empaquetadas por esbuild

Vite eligió esbuild para dev por pura latencia, y reservó Rollup para el build por su calidad de salida y su ecosistema de plugins. Esa dualidad —un motor rapidísimo delante, uno maduro detrás— definió la arquitectura de Vite durante años.

El reparto de tareas en desarrollo conviene nombrarlo con precisión:

  • Pre-empaquetar dependencias: escanear los imports de node_modules, fusionar en unos pocos los cientos de archivos internos de cada paquete y convertir a ESM el CommonJS heredado.
  • Transpilar al vuelo: borrar tipos y convertir JSX de cada módulo fuente en el instante exacto en que el navegador lo pide, sin tocar nada más.

En desarrollo, las limitaciones de esbuild como bundler no dolían: servir módulos sueltos a un navegador que ya sabe recorrer el grafo no exige un tree shaking perfecto ni un troceado fino. La calidad de salida solo pesaba en producción, y esa mitad se la quedaba Rollup.

# El reparto de motores en el Vite clasico (v2 a v6)
dev    ->  esbuild en Go: pre-bundling y transpilacion, elegido por latencia
build  ->  Rollup en JS: tree shaking, chunks y plugins, elegido por calidad

Merece subrayar lo elegante de esta separación. Vite no intentó que un solo motor fuera perfecto en todo; asignó a cada mitad el que mejor la servía y dejó que su propia interfaz los uniera. Esa idea —estabilizar la interfaz y cambiar el motor debajo— es la que, años después, permitiría reemplazar ambos motores por Rolldown sin que el usuario tuviera que reaprender nada.

🐹

esbuild transformador

Borra tipos de TypeScript y convierte JSX a la velocidad de Go. El sustituto directo de Babel en la mitad de dev de Vite.

📦

esbuild bundler

Recorre el grafo, hace tree shaking y produce un artefacto. Pre-empaquetaba node_modules con optimizeDeps.

Una sola pasada

Parsea una vez y reutiliza el AST en cada fase. Sin serializar, sin releer, sin cruzar procesos.

flowchart LR
src[Codigo TS y JSX] --> p[Parser unico]
p --> ast[Un solo AST en memoria]
ast --> t[Transforma y borra tipos]
ast --> b[Empaqueta el grafo]
ast --> m[Minifica la salida]
t --> out[Artefacto final]
b --> out
m --> out
style ast fill:#a6e3a1,color:#11111b
style out fill:#89b4fa,color:#11111b

Los límites que marcaron su papel

Si esbuild era tan veloz, cabe preguntarse por qué Vite no lo usó también para producción. La respuesta está en sus límites deliberados. Su control de la salida —el troceado fino en chunks, la granularidad del code splitting— era menos expresivo que el de Rollup. Su modelo de plugins, más pobre que los hooks que Rollup había convertido en estándar de facto. Y su tree shaking, excelente para la mayoría de casos, no cubría ciertos rincones que Rollup sí exprimía. esbuild optimizó por velocidad y simplicidad; Rollup, por calidad y flexibilidad. Vite tomó lo mejor de cada uno. Los puntos concretos donde esbuild cedía eran nítidos:

  • Un code splitting menos granular y con menos control sobre los chunks resultantes.
  • Un modelo de plugins más pobre que los hooks que Rollup había vuelto estándar.
  • Un tree shaking excelente en el caso común, pero menos exhaustivo en los rincones.
  • Menos ganchos para las reescrituras finas que un artefacto de producción a veces pide.

Ninguna de estas carencias era un defecto: eran el precio consciente de la simplicidad y la velocidad. esbuild renunció a la última milla de control para ganar el primer kilómetro de rapidez, y para el dev de Vite ese canje era exactamente el correcto.

Esa división del trabajo —esbuild en dev, Rollup en build— fue exactamente la costura que Vite 8 disuelve al sustituir ambos por Rolldown en Rust. Pero que esbuild cediera su trono en Vite no significa que desapareciera: sigue incrustado en incontables herramientas, corriendo scripts de build, transpilando en entornos de test y sirviendo de motor a proyectos que nunca lo nombran. Algunos lugares donde sigue trabajando hoy:

  • tsx y otros ejecutores que corren TypeScript al vuelo sin configuración.
  • Cadenas de build de librerías que quieren un empaquetado rápido y simple.
  • Entornos de test que transpilan cada archivo antes de ejecutarlo.
  • Herramientas que lo incrustan como transformador por su API estable en Go.
ℹ️
esbuild no muere, se retira del centro

Conviene no leer la llegada de Rolldown como el fin de esbuild. Lo que termina es su papel dentro de Vite, no su vida. esbuild sigue siendo la forma más simple de empaquetar una librería, transpilar un archivo suelto o montar un build rápido sin arrastrar un bundler pesado. Herramientas como tsx lo usan por debajo para ejecutar TypeScript al vuelo. Su cuota de protagonismo baja, pero su ubicuidad como pieza discreta sigue creciendo.

esbuild probó que el peaje era opcional, y esa idea sobrevive a esbuild

La contribución histórica de esbuild no es una cifra de rendimiento, sino un cambio de expectativas. Antes de él, la lentitud del tooling de JavaScript se aceptaba como una constante de la naturaleza: transpilar y empaquetar eran caros porque siempre lo habían sido. esbuild demostró que buena parte de ese coste era un peaje evitable, el precio de escribir las herramientas en el mismo lenguaje interpretado que procesaban, en un solo hilo y reparseando el código en cada etapa. Al reescribir todo en Go —compilado, paralelo, con el AST compartido de punta a punta— no mejoró la carrera: la reencuadró. Y aquí está la lección profunda que debes llevarte de esta lección. esbuild acabó cediendo su lugar en Vite a Rolldown, un motor en Rust todavía más rápido y capaz. Perdió el trono, sí, pero ganó la discusión: cada motor serio que vino después —SWC en Rust, Oxc en Rust, el propio Rolldown— es hijo de la tesis que esbuild probó primero, la de que un lenguaje nativo y una sola pasada bastan para convertir minutos en milisegundos. No memorices esbuild como la herramienta rápida de 2020; entiéndelo como el momento en que el ecosistema dejó de aceptar la lentitud como destino. Las herramientas de moda pasan; las ideas que reencuadran un problema se quedan, incluso cuando el binario que las encarnó cede el paso al siguiente.

⚔️ Palpa las dos naturalezas de esbuild
  1. Instala esbuild y transpila un .tsx suelto con --loader=tsx: observa cómo desaparecen los tipos y el JSX se convierte en llamadas de función.
  2. Ahora empaqueta ese mismo archivo con --bundle --minify y compara el resultado: la misma herramienta, un oficio distinto.
  3. En un proyecto Vite clásico, inspecciona node_modules/.vite/deps/ y cuenta cuántos archivos hay frente a los cientos de módulos internos de tus dependencias.
  4. Cronometra la transpilación de una carpeta grande con esbuild y con Babel: mide tú mismo el orden de magnitud, no te fíes de la cifra prometida.
  5. Explica en dos frases por qué esbuild sirvió para dev en Vite pero Rollup se quedó con el build de producción.