wandres.dev
MINIFICACIÓN · terser, esbuild, oxc

terser, esbuild y oxc-minify: el trade-off velocidad/tamaño

Tres motores para la misma tarea: terser en JavaScript busca la máxima compresión al precio de la lentitud, esbuild en Go prioriza la velocidad bruta a cambio de unos bytes de más, y oxc-minify en Rust aspira en 2026 a dominar ambos ejes. Por qué existe una frontera de Pareto entre velocidad y tamaño, y por qué esa frontera se aplana cuando mides el resultado ya comprimido.

⏱ 16 min

Todos los minificadores hacen lo mismo —quitar espacios, acortar nombres, podar lo muerto—, pero no todos lo hacen igual de bien ni igual de rápido, y ahí nace una decisión de ingeniería con consecuencias reales sobre tu tiempo de build y sobre los bytes que envías. terser es el veterano que exprime cada byte a costa de ser lento; esbuild es el velocista que sacrifica un poco de tamaño por ir cien veces más deprisa; oxc-minify, el recién llegado en Rust, aspira a no ceder en ninguno de los dos frentes. Elegir bien exige entender que velocidad y tamaño son ejes en tensión, y que el eje que de verdad importa a tu usuario no es ninguno de los dos que el minificador optimiza directamente.

🎯 Al terminar esta lección sabrás
  • Situar terser, esbuild y oxc-minify por su lenguaje, su velocidad y su agresividad de compresión.
  • Entender el trade-off velocidad/tamaño como una frontera de Pareto entre motores.
  • Saber qué minificador usa Vite por defecto y cómo cambiarlo.
  • Comprender por qué la ventaja de tamaño se aplana al medir el resultado ya comprimido.

Tres motores para una misma tarea

terser es el patrón oro de la compresión. Heredero de UglifyJS —que solo entendía ES5— fue reescrito para hablar JavaScript moderno, y está escrito en JavaScript. Ofrece el compress y el mangle más agresivos del ecosistema, con decenas de opciones y hasta varias pasadas sobre el mismo código. Ese refinamiento se paga en tiempo: analizar y reescribir el AST en un lenguaje de un solo hilo es lento, y en un proyecto grande el paso de minificado puede volverse el cuello de botella del build.

esbuild invirtió la ecuación. Escrito en Go, compilado y paralelo, minifica como parte de su propia generación de código a una velocidad de uno a dos órdenes de magnitud sobre terser. Su salida es un poco mayor —un pequeño porcentaje— porque no persigue cada micro-optimización, pero a cambio el minificado deja de notarse en el reloj. Expone la operación de forma granular con minifyWhitespace, minifyIdentifiers y minifySyntax, por si quieres activar solo una parte.

oxc-minify es la apuesta de 2026. Forma parte de Oxc —el Oxidation Compiler, en Rust— y su ambición declarada es alcanzar una compresión cercana a la de terser a una velocidad del orden de la de esbuild. Todavía madura, pero viaja dentro de Rolldown, así que a medida que Rolldown se convierte en el motor de Vite 8, oxc-minify pasa a ser el minificador nativo de todo el stack, sin instalar nada aparte.

🎯

terser en JavaScript

Máxima compresión. El compress y el mangle más finos, con pasadas múltiples. Lento por ser JS de un solo hilo, pero sigue siendo la referencia de tamaño.

🏎️

esbuild en Go

Velocidad bruta. Minifica dentro de su codegen, cien veces más rápido, a cambio de unos pocos bytes de más. El valor por defecto histórico de Vite.

🦀

oxc-minify en Rust

El emergente. Aspira a la compresión de terser a la velocidad de Rust. Viaja en Rolldown y se vuelve nativo del stack con Vite 8.

Conviene nombrar también a swc, el otro minificador en Rust nacido antes que Oxc: existe y funciona, pero en 2026 el impulso del ecosistema converge sobre Oxc por su integración con Rolldown y Vite. La historia se repite: primero JavaScript, luego Go, ahora Rust, cada salto recortando un orden de magnitud.

Ese relevo de motores deja una pregunta práctica: dado que los tres hacen la misma tarea, ¿qué decide cuál usar? Tres ejes, y ninguno es “cuál comprime más en abstracto”:

  • Cuánto pesa tu tiempo de build, que pagas en cada commit y en cada despliegue.
  • Cuántas veces se descarga tu salida, que multiplica el valor de cada byte ahorrado.
  • Qué motor es nativo de tu bundler, porque integrarse ahorra un parseo y una dependencia.

El trade-off velocidad/tamaño

Los tres motores ocupan puntos distintos de una misma frontera de Pareto. terser vive en el extremo de tamaño mínimo y tiempo máximo; esbuild, en el de tiempo mínimo y tamaño ligeramente mayor; oxc-minify intenta lo difícil, empujar la frontera hacia dentro para no ceder en ninguno. Que exista frontera significa que, con las herramientas de una misma generación, ganar tamaño cuesta tiempo y ganar tiempo cuesta tamaño.

Conviene nombrar los ejes que esa frontera enfrenta:

  • Tiempo de build: cuánto tarda el minificador; lo pagas tú, en cada compilación.
  • Tamaño de salida: cuántos bytes produce; lo paga el usuario, en cada descarga.
  • Agresividad: cuántas transformaciones intenta; más ahorro a cambio de más tiempo.
  • Integración: si es un paso aparte o parte del bundler, lo que cambia la velocidad total.
flowchart TD
P[Que priorizas en el build] --> V{Velocidad o tamano}
V -->|tamano minimo| T[terser mas lento]
V -->|velocidad bruta| E[esbuild muy rapido]
V -->|ambos a la vez| O[oxc-minify emergente]
style T fill:#cba6f7,color:#11111b
style E fill:#f9e2af,color:#11111b
style O fill:#a6e3a1,color:#11111b

Dentro de terser, ese trade-off aparece incluso en una sola opción: compress.passes. Cada pasada adicional vuelve a recorrer el código en busca de nuevas oportunidades que la anterior abrió, y suele arañar algún byte más, pero con rendimientos decrecientes y un coste de tiempo lineal. Dos pasadas es un compromiso común; cinco rara vez justifican la espera.

Puesto en una tabla, cada motor ocupa un punto distinto del compromiso:

Motor Lenguaje Velocidad Tamaño crudo
terser JavaScript lento el más pequeño
esbuild Go muy rápido algo mayor
oxc-minify Rust rápido cercano a terser

El trade-off se afina con unas pocas palancas, todas con rendimientos decrecientes:

  • Número de pasadas: cada una araña bytes, pero el tiempo crece y el ahorro se agota.
  • Agresividad del compress: más reglas activas, más tiempo de análisis por un puñado de bytes.
  • mangle de propiedades: ahorra de verdad, pero solo si asumes su riesgo con listas de reservados.
  • Paralelismo del motor: lo que uno en Rust o Go reparte entre núcleos, terser lo hace en un solo hilo.
// vite.config.ts — elegir y afinar el minificador
export default defineConfig({
  build: {
    minify: "terser",           // por defecto seria "esbuild"
    terserOptions: {
      compress: { passes: 2 },  // rendimientos decrecientes a partir de aqui
      mangle: true,
    },
  },
});

Qué elige tu herramienta, y qué deberías medir

Vite trae esbuild como minificador por defecto —build.minify vale "esbuild"— justamente porque el tiempo de build importa y la diferencia de tamaño es pequeña. Poner "terser" es opt-in y exige instalarlo aparte; tiene sentido cuando cada kilobyte cuenta y el build puede permitirse la lentitud. Con Vite 8 y Rolldown, el cuadro se desplaza: el minificador nativo pasa a ser oxc-minify, y la elección deja de ser entre lento-pequeño y rápido-grande.

# terser no viene incluido: instalalo si vas a usarlo
npm install -D terser
# esbuild ya viaja dentro de Vite; oxc-minify llega con Rolldown

esbuild permite además activar solo una parte del minificado, útil cuando quieres apretar espacios pero conservar los nombres para poder leer el bundle:

// esbuild expone la operacion de forma granular
export default defineConfig({
  esbuild: {
    minifyWhitespace: true,
    minifyIdentifiers: false,   // conserva los nombres legibles
    minifySyntax: true,
  },
});
📝
En esbuild y Rolldown, minificar no es una etapa aparte

A diferencia de terser, que es un minificador puro que corre después del bundler, esbuild y Rolldown minifican durante su propia generación de código. No hay un paso separado que reparse el bundle: el mismo motor que empaqueta emite ya la salida apretada. Eso explica buena parte de su ventaja de velocidad —se ahorran un parseo completo— y también por qué su minificado está tan atado a la versión del bundler que uses.

La decisión práctica se reduce a unos pocos escenarios:

  • App de producto en iteración rápida: esbuild u oxc-minify, porque el tiempo de build se paga en cada commit.
  • Librería muy descargada: terser con varias pasadas, porque el byte ahorrado se multiplica por millones de descargas.
  • Presupuesto de bytes al límite: la máxima compresión disponible, midiendo siempre el tamaño comprimido.
  • Monorepo con builds frecuentes: el motor más rápido, y la compresión del servidor haciendo el resto.
💡
Mide el tamaño comprimido, no el crudo

Aquí está la trampa que descoloca a mucha gente. La ventaja de tamaño de terser sobre esbuild se mide en el archivo crudo, pero tu usuario nunca descarga el archivo crudo: descarga el resultado tras pasar por gzip o brotli en el servidor. Y buena parte de lo que terser ahorra de más es redundancia que el compresor también habría eliminado por su cuenta. El resultado es que una diferencia del cinco por ciento en crudo puede encogerse a menos del uno por ciento tras comprimir. La cifra honesta, la única que paga tu usuario, es el tamaño ya comprimido. Cronometra el build y mide el .br, no el .js pelado.

Se estabilizan las interfaces y se reemplazan los motores; tú apuesta por medir, no por la moda

La saga de los minificadores es un microcosmos de la ley que gobierna todo el toolchain moderno: la tarea se estabiliza —quitar espacios, acortar nombres, podar lo muerto— mientras el motor que la ejecuta se reemplaza cada pocos años por otro más rápido en un lenguaje más cercano al metal. JavaScript con UglifyJS y terser, Go con esbuild, Rust con oxc-minify: tres generaciones, un mismo trabajo, cada una recortando un orden de magnitud sin cambiar lo que significa minificar. Quien aprendió qué hace un minificador en 2018 sigue vigente en 2026 sin reaprender nada, porque aprendió el cimiento y no el andamio. Pero la lección más afilada no es sobre los motores, sino sobre qué optimizas. Es tentador pelearse por el minificador que produce el crudo más pequeño, como si esos bytes fueran el objetivo. No lo son. El objetivo es lo que el usuario descarga, y eso llega tras una segunda capa —la compresión del servidor— que aplana buena parte de la ventaja de compresión del minificador. La consecuencia estratégica es contraintuitiva: para la mayoría de proyectos, la elección correcta en 2026 es el minificador más rápido cuyo tamaño comprimido sea competitivo, porque el tiempo de build es un coste que pagas en cada commit y los pocos bytes que terser gana de más se evaporan tras brotli. Reserva la máxima compresión para donde de verdad domina —una librería que se descarga millones de veces, un presupuesto de bytes al límite— y en el resto deja que la velocidad mande. La madurez no es elegir el motor de moda ni el más agresivo por defecto: es medir el número que tu usuario paga, el tamaño comprimido, y dejar que ese número decida por ti.

⚔️ Pesa y cronometra los tres
  1. Minifica un mismo bundle con esbuild y con terser y anota los dos tamaños en crudo y los dos tiempos de build.
  2. Comprime ambas salidas con gzip -9 y con brotli -q 11 y compara: ¿cuánto de la ventaja de terser sobrevive a la compresión?
  3. Sube compress.passes de 1 a 3 en terser y mide qué byte ganas por cada pasada y cuánto tiempo cuesta: dibuja los rendimientos decrecientes.
  4. Prueba oxc-minify mediante Rolldown o rolldown-vite sobre el mismo proyecto y sitúalo en el eje velocidad/tamaño frente a los otros dos.
  5. Con los cuatro números en la mano —tiempo y tamaño comprimido— justifica en dos frases qué minificador elegirías para una app de producto y cuál para una librería muy descargada.