Bun 1.4.1: tu bundle de zod pesa un 79% menos sin tocar código
Hace unas semanas abrí el análisis de bundle de un backend que despliego como ejecutable compilado desde hace dos años. Hono, Postgres, validación con zod. Nada exótico.
Lo que me llamó la atención no fue el tamaño total, fue el reparto. Zod se llevaba una porción absurda para los cuatro schemas que usaba de verdad.
La explicación estaba en un await import() dinámico enterrado en un módulo de rutas. Ese import funcionaba como un muro: el bundler no podía mirar al otro lado, así que metía el módulo entero por si acaso.
Bun 1.4.1 tira ese muro. El número que publica el equipo de Bun es exactamente el que me interesaba: zod pasa de 375,3 KB a 77,3 KB. Un 79% menos sin tocar una línea de código de aplicación.
El titular fácil de esta release es "202 issues cerrados". Ese no es el titular. El titular es que el bundler de Bun ha dejado de ser su eslabón flojo.
Bun 1.4.1 se publicó el 4 de septiembre de 2026 y su cambio principal está en el bundler: bun build ya hace tree-shaking a través de import() dinámico y de export * as. Medido por el equipo de Bun sobre librerías reales, zod 4.5 baja de 375,3 KB a 77,3 KB (79% menos), effect 3.22 de 369,1 KB a 163,6 KB (56% menos) y fp-ts 2.16 de 21,8 KB a 3,2 KB (85% menos). En el runtime llegan HTTP/2 y HTTP/1.1 en el mismo puerto de Bun.serve(), Bun.write() con streaming a disco y crypto.argon2(). Se actualiza con bun upgrade, y las notas no anuncian ningún breaking change: son 202 issues cerrados sobre la 1.4.0.
Todas las cifras de este post salen de las notas oficiales de la release de Bun v1.4.1.
Tree-shaking a través de import() dinámico: el gran cambio de Bun 1.4.1
Hasta ahora un import dinámico era una frontera para el bundler: sabía que el módulo existía, pero no qué se usaba de él, así que la única opción segura era incluirlo entero.
Mira este caso, sacado de las notas de la release:
// is.ts exports isNumber, isOdd, and isEven
const { isOdd } = await import("./is");
console.log(isOdd(3));
Antes, ese import() arrastraba isNumber e isEven al bundle aunque nadie las llamara nunca. Ahora Bun analiza el destructuring y solo entra isOdd.
Parece un detalle de juguete. Multiplícalo por una librería con cientos de exports y entiendes el 79% menos de zod.
Y ahí está lo interesante: los patrones de carga perezosa que usábamos para "no cargar la validación hasta que haga falta" llevaban años penalizándonos. Cargábamos tarde, sí, pero cargábamos todo.
En el curso de Zod para TypeScript trabajo justo esa parte: componer schemas por módulo en lugar de un barrel gigante que el bundler tiene que adivinar.
export * as: el otro sitio donde se escondía el peso
El segundo sitio donde Bun 1.4.1 recorta peso es más silencioso: los re-exports.
export * as algo from "./modulo" es el re-export de namespace: agrupas un módulo entero bajo un nombre y lo sacas por tu index.ts. Cómodo para importar, veneno para el tamaño final.
Bun 1.4.1 optimiza ese re-export. El resultado con fp-ts 2.16: de 21,8 KB a 3,2 KB. Un 85% menos.
Si tus barrels reexportan con export * as —y en arquitectura por features es habitual agrupar así—, este cambio te afecta aunque no lo hayas pedido. Si solo usas export * plano, mide antes de celebrar.
Code splitting y ejecutables que arrancan antes
El code splitting también mejora. El dashboard de Medusa pasa de 349 a 245 archivos JS generados: menos peticiones, menos overhead y menos coste de arranque en el navegador.
Y en builds de navegador con --splitting, Bun ahora emite module preloading, así que los chunks se piden en paralelo en lugar de en cascada.
La parte que más me sorprendió está en los ejecutables compilados: arrancan alrededor de un 20% más rápido, con el bytecode más compacto según el equipo de Bun. El caso que usan de ejemplo es Claude Code: de 397 ms a 318 ms de arranque, y la instalación baja de 376 MB a 207 MB.
Ochenta milisegundos escasos suenan a nada hasta que recuerdas cuántas veces al día lanzas tu CLI favorita. Y fíjate en el detalle: Claude Code, el ejemplo que usa Bun para medirse, está compilado con Bun. Si vives en la terminal con herramientas así —el flujo que enseño en Construye con IA—, ese 20% lo notas en la fricción, no en el benchmark.
Bun.serve() con HTTP/2 y HTTP/1.1 en el mismo puerto
En el runtime, Bun 1.4.1 trae dos cambios que afectan a código real: HTTP/2 en Bun.serve() y Bun.write() con streaming a disco.
El primero es un flag:
Bun.serve({
tls: { key, cert },
http2: true,
fetch(req) {
return new Response("hi");
},
});
Sin proxy delante, sin segundo servidor, sin decidir de antemano qué protocolo habla tu cliente.
Si todavía estás en la pregunta anterior a esta —por qué Bun y no Node—, la respuesta larga la escribí en Bun reemplazando a Node.js en backend TypeScript. Aquí doy por hecho que ya la tienes resuelta.
El segundo cambio: Bun.write() hace streaming a disco en vez de bufferizar en memoria.
await Bun.write("./big.tar.gz", await fetch(url)); // => bytes escritos
await Bun.write("./out.txt", readableStream); // antes escribía "[object ReadableStream]"
En una descarga de 128 MiB, el pico de RSS baja de 161 MB a 13 MB. Ese primer ejemplo es el patrón que todos escribimos alguna vez para guardar un archivo remoto, y hasta hoy cargaba el archivo entero en RAM antes de tocar el disco.
La segunda línea es directamente un bug arreglado: pasar un ReadableStream escribía el texto "[object ReadableStream]" en tu fichero. Si tienes archivos corruptos en producción con ese contenido exacto, ya sabes de dónde venían.
También hay pause(), resume() y la propiedad isPaused en el WebSocket cliente, que es la pieza que faltaba para hacer backpressure decente:
import { createWriteStream } from "node:fs";
const file = createWriteStream("./feed.ndjson");
const socket = new WebSocket("wss://example.com/feed");
socket.addEventListener("message", (event) => {
if (!file.write(event.data)) {
socket.pause();
file.once("drain", () => socket.resume());
}
});
Sin eso, un feed rápido y un disco lento acababan siempre en el mismo sitio: memoria creciendo hasta que algo revienta.
Memoria y detalles que se notan a las tres de la mañana
La memoria en reposo baja. Un SSR de Next.js pasa de 222 MB a 142 MB una vez terminada la carga. En un contenedor con límite de 256 MB, eso es la diferencia entre dormir tranquilo y recibir alertas de OOM.
Ojo con la lectura fácil de este tipo de cifras: lo mismo pasó con el 90% menos de memoria de Next.js 16.3, donde el número era real pero no era el de todo el mundo.
En Buffer hay dos mejoras concretas, y quiero ser preciso porque esto se lee por ahí como "Buffer 9x más rápido": son 9,2x en writeFloatLE() y 7,2x en writeUInt8(). Métodos específicos, no la clase entera.
AsyncLocalStorage va unas 2x más rápido y ya no asigna memoria en cada await. Si tienes tracing o contexto de request atravesando toda la aplicación, esto lo estabas pagando en cada salto asíncrono.
Y llegan crypto.argon2() y crypto.argon2Sync(). Bun ya hasheaba con argon2id desde Bun.password; lo nuevo es tenerlo en la API de crypto, que es lo que te ahorra reescribir el módulo de auth cuando portas código de Node.
Todos los números de Bun 1.4.1, en una tabla
| Caso medido | Antes | Bun 1.4.1 | Mejora |
|---|---|---|---|
| Bundle de zod 4.5 | 375,3 KB | 77,3 KB | 79% menos |
| Bundle de effect 3.22 | 369,1 KB | 163,6 KB | 56% menos |
export * as con fp-ts 2.16 |
21,8 KB | 3,2 KB | 85% menos |
| Archivos JS del dashboard de Medusa | 349 | 245 | 30% menos |
| Arranque de Claude Code | 397 ms | 318 ms | 20% menos |
| Instalación de Claude Code | 376 MB | 207 MB | 45% menos |
| Pico de RSS al descargar 128 MiB | 161 MB | 13 MB | 92% menos |
| Memoria en reposo de un SSR de Next.js | 222 MB | 142 MB | 36% menos |
Buffer.writeFloatLE() |
2,85 ns | 0,31 ns | 9,2x |
Buffer.writeUInt8() |
2,24 ns | 0,31 ns | 7,2x |
Fuente: notas de la release de Bun v1.4.1.
bun install --offline: es para tu CI, no para ti
Dos flags nuevos en bun install.
--offline falla si algo no está en caché. Cero red, sin excusas. --prefer-offline es la versión suave: usa la caché y se salta el refetch de metadatos, pero baja lo que falte.
Los mismos valores viven en bunfig.toml:
# bunfig.toml
[install]
offline = true # equivale a --offline
# prefer = "offline" # equivale a --prefer-offline
--offline en local te va a molestar. En CI, con la caché restaurada antes del install, convierte un fallo de red del registry en un error reproducible en lugar de un build rojo aleatorio a las once de la noche.
Y para monorepos, los workspaces admiten paquetes con node_modules autocontenidos:
{
"workspaces": {
"packages": ["apps/*"],
"selfContained": ["apps/desktop"]
}
}
Útil cuando una app del monorepo se distribuye sola —un binario de escritorio, un contenedor— y necesita sus dependencias dentro, no hoisted arriba del todo.
Si vienes de otro gestor, el movimiento de fondo es el mismo en todo el ecosistema: velocidad y builds reproducibles. Lo analicé cuando salió pnpm 12 reescrito en Rust.
¿Merece la pena actualizar a Bun 1.4.1? Qué migrar hoy y qué no
Bun 1.4.1 no es una revolución. Es una release de consolidación, y el bloque del bundler es lo único que justifica que la instales esta semana.
| Tu situación | Qué hacer con Bun 1.4.1 | Por qué |
|---|---|---|
Compilas frontend o librerías con bun build |
Actualiza hoy | La ganancia de tamaño es gratis y se mide en diez minutos |
Distribuyes ejecutables con bun build --compile |
Actualiza hoy | 20% menos de arranque y 45% menos de instalación sin tocar código |
| Bun solo ejecuta tu servidor | Actualiza sin prisa | HTTP/2 y menos RSS no arreglan nada que hoy funcione |
| Estás en Node y te funciona | No migres | Una release no justifica cambiar de runtime en producción |
La primera fila es la que tiene premio inmediato: actualizas, relanzas el build y comparas el tamaño. Diez minutos. La tercera puede esperar al próximo sprint, porque HTTP/2 en el mismo puerto y menos RSS están muy bien, pero no arreglan nada que hoy funcione.
Y no migres de Node a Bun solo por esta release. Si Node te sirve, esto no cambia la ecuación. Lo que cambia es que el argumento "el bundler de Bun todavía no está maduro" ya no se sostiene.
Mi orden de trabajo esta semana es este: actualizar, lanzar el build, medir el bundle antes y después, y revisar dónde teníamos import() dinámicos puestos como optimización que en realidad no optimizaban nada.
Ese ejercicio de medir antes y después es de lo que más discutimos en Dominicode Labs, porque casi siempre aparece algo que llevaba años ahí sin que nadie lo mirara.
Preguntas frecuentes
¿Tengo que cambiar código para ganar el 79% menos de zod?
No. La optimización ocurre al empaquetar, no al escribir: actualizas Bun, vuelves a lanzar bun build y el bundle sale más pequeño. Lo único que conviene revisar es si tus import() dinámicos destructuran lo que usan de verdad, porque cuanto más explícito seas al importar, más trabajo puede hacer el bundler.
¿El tree-shaking a través de import() dinámico funciona fuera de Bun?
No. Es una optimización del bundler de Bun, no del lenguaje ni de TypeScript, así que el beneficio solo llega cuando el build lo hace bun build. Ejecutar tu aplicación con Bun pero empaquetar con otro bundler no te da estos números.
¿HTTP/2 en Bun.serve() necesita TLS?
Sí en la práctica: el ejemplo oficial de la release configura tls junto a http2: true, que es el escenario normal para HTTP/2 en internet. Lo nuevo no es soportar el protocolo, es que HTTP/2 y HTTP/1.1 conviven en el mismo puerto, sin levantar dos servidores ni decidir por adelantado qué habla el cliente.
¿Cómo actualizo a Bun 1.4.1 y hay breaking changes?
Con bun upgrade, y las notas de la release no anuncian ningún breaking change: es una versión de consolidación que cierra 202 issues sobre la 1.4.0. Si compilas con bun build, el orden sensato es actualizar, relanzar el build y comparar el tamaño antes y después.
¿bun install –offline sirve para CI?
Sí, es su mejor uso: restauras la caché de Bun al principio del job y lanzas bun install --offline, de modo que si falta algo el build falla ahí y no a mitad del pipeline. Si prefieres algo menos estricto, --prefer-offline se salta el refetch de metadatos pero descarga lo que no esté en caché.
¿Merece la pena migrar de Node a Bun solo por esta release?
No. Una release no justifica una migración de runtime en un proyecto que ya está en producción y funciona. Lo que sí merece la pena es probar bun build como bundler aunque ejecutes con Node: esa prueba cuesta una tarde y te da datos de tu proyecto en lugar de benchmarks ajenos.
Si quieres ver este tipo de análisis en vídeo, con el proyecto delante y midiendo en directo, lo publico en el canal de YouTube de Dominicode.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
