Saltar al contenido
28 min de lectura

Bun en producción: qué funciona, qué no y qué me sorprendió

Bun como runtime, gestor de paquetes, bundler y test runner. Benchmarks reales, brechas de compatibilidad con Node.js, patrones de migración y dónde uso Bun en producción hoy.

Compartir:X / TwitterLinkedIn

Cada pocos años, el ecosistema JavaScript recibe un nuevo runtime y el discurso sigue un arco predecible. Hype. Benchmarks. "X ha muerto." Dosis de realidad. Acomodarse en los casos de uso reales donde la nueva herramienta genuinamente brilla.

Bun está en medio de ese arco ahora mismo. Y a diferencia de la mayoría de los retadores, se mantiene firme. No porque sea "más rápido" (aunque frecuentemente lo es), sino porque está resolviendo un problema genuinamente diferente: la cadena de herramientas de JavaScript tiene demasiadas piezas móviles, y Bun las colapsa en una sola.

He estado usando Bun en diversas capacidades durante más de un año. Parte en producción. Parte reemplazando herramientas que pensé que jamás reemplazaría. Este artículo es un recuento honesto de qué funciona, qué no y dónde las brechas aún importan.

Qué es realmente Bun#

La primera idea errónea que hay que aclarar: Bun no es "un Node.js más rápido." Esa descripción lo subestima.

Bun son cuatro herramientas en un solo binario:

  1. Un runtime de JavaScript/TypeScript — ejecuta tu código, como Node.js o Deno
  2. Un gestor de paquetes — reemplaza a npm, yarn o pnpm
  3. Un bundler — reemplaza a esbuild, webpack o Rollup para ciertos casos de uso
  4. Un test runner — reemplaza a Jest o Vitest para la mayoría de las suites de tests

La diferencia arquitectónica clave respecto a Node.js es el motor. Node.js usa V8 (el motor de Chrome). Bun usa JavaScriptCore (el motor de Safari). Ambos son motores maduros y de nivel de producción, pero hacen compromisos diferentes. JavaScriptCore tiende a tener tiempos de arranque más rápidos y menor overhead de memoria. V8 tiende a tener mejor rendimiento pico para computaciones de larga duración. En la práctica, estas diferencias son más pequeñas de lo que pensarías para la mayoría de las cargas de trabajo.

El otro gran diferenciador: Bun está escrito en Zig, un lenguaje de programación de sistemas que se sitúa aproximadamente al mismo nivel que C pero con mejores garantías de seguridad de memoria. Por eso Bun puede ser tan agresivo con el rendimiento — Zig te da el tipo de control de bajo nivel que proporciona C sin la densidad de trampas de C.

bash
# Verificar tu versión de Bun
bun --version
 
# Ejecutar un archivo TypeScript directamente — sin tsconfig, sin paso de compilación
bun run server.ts
 
# Instalar paquetes
bun install
 
# Ejecutar tests
bun test
 
# Empaquetar para producción
bun build ./src/index.ts --outdir ./dist

Eso es un solo binario haciendo el trabajo de node + npm + esbuild + vitest. Te guste o no, es una reducción convincente de complejidad.

Las afirmaciones de velocidad — Benchmarks honestos#

Permíteme ser directo con esto: los benchmarks de marketing de Bun están cuidadosamente seleccionados. No son fraudulentos — están seleccionados. Muestran los escenarios donde Bun rinde mejor, que es exactamente lo que esperarías del material de marketing. El problema es que la gente extrapola de esos benchmarks para afirmar que Bun es "25x más rápido" en todo, lo cual absolutamente no es.

Aquí es donde Bun es genuina y significativamente más rápido:

Tiempo de arranque#

Esta es la mayor ventaja genuina de Bun y ni siquiera es una competencia reñida.

bash
# Midiendo el tiempo de arranque — ejecutar cada uno 100 veces
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Resultados típicos:
# node:  ~40ms
# bun:   ~6ms

Eso es aproximadamente una diferencia de 6-7x en tiempo de arranque. Para scripts, herramientas CLI y funciones serverless donde el arranque en frío importa, esto es significativo. Para un proceso de servidor de larga duración que arranca una vez y corre por semanas, es irrelevante.

Instalación de paquetes#

Esta es la otra área donde Bun humilla a la competencia.

bash
# Benchmark de instalación limpia — eliminar node_modules y lockfile primero
rm -rf node_modules bun.lockb package-lock.json
 
# Cronometrar npm
time npm install
# Real: ~18.4s (proyecto de tamaño medio típico)
 
# Cronometrar bun
time bun install
# Real: ~2.1s

Eso es una diferencia de 8-9x, y es consistente. Las razones son principalmente:

  1. Lockfile binariobun.lockb es un formato binario, no JSON. Más rápido de leer y escribir.
  2. Caché global — Bun mantiene una caché global de módulos para que las reinstalaciones entre proyectos compartan los paquetes descargados.
  3. I/O de Zig — El propio gestor de paquetes está escrito en Zig, no en JavaScript. Las operaciones de I/O de archivos están más cerca del metal.
  4. Estrategia de symlinks — Bun usa hardlinks desde la caché global en lugar de copiar archivos.

Rendimiento del servidor HTTP#

El servidor HTTP integrado de Bun es rápido, pero las comparaciones necesitan contexto.

bash
# Benchmark rápido con bombardier
# Probando una respuesta simple "Hello World"
 
# Servidor Bun
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
 
# Node.js (módulo http nativo)
bombardier -c 100 -d 10s http://localhost:3001
# Requests/sec: ~48,000
 
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# Requests/sec: ~15,000

Bun vs. Node.js puro: aproximadamente 2x para respuestas triviales. Bun vs. Express: aproximadamente 7x, pero eso es injusto porque Express añade overhead de middleware. En el momento que añades lógica real — consultas a base de datos, autenticación, serialización JSON de datos reales — la brecha se reduce drásticamente.

Dónde la diferencia es insignificante#

Computación ligada a CPU:

typescript
// fibonacci.ts — esto depende del motor, no del runtime
function fib(n: number): number {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
 
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);
bash
bun run fibonacci.ts   # ~1650ms
node fibonacci.ts      # ~1580ms

Node.js (V8) realmente gana ligeramente aquí. El compilador JIT de V8 es más agresivo en bucles calientes. Para trabajo ligado a CPU, las diferencias de motor son un empate — a veces V8 gana, a veces JSC gana, y las diferencias están dentro del ruido.

Cómo ejecutar tus propios benchmarks#

No confíes en los benchmarks de nadie, incluidos los míos. Así es como medir lo que importa para tu carga de trabajo específica:

bash
# Instalar hyperfine para benchmarking adecuado
brew install hyperfine  # macOS
# o: cargo install hyperfine
 
# Benchmark de arranque + ejecución de tu app real
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# Para servidores HTTP, usa bombardier o wrk
# Importante: prueba con payloads realistas, no "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# Comparación de memoria
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

La regla general: si tu cuello de botella es I/O (sistema de archivos, red, base de datos), la ventaja de Bun es modesta. Si tu cuello de botella es el tiempo de arranque o la velocidad de la cadena de herramientas, Bun gana por mucho. Si tu cuello de botella es la computación pura, es un empate.

Bun como gestor de paquetes#

Aquí es donde he cambiado completamente. Incluso en proyectos donde ejecuto Node.js en producción, uso bun install para desarrollo local y CI. Es simplemente más rápido, y la compatibilidad es excelente.

Lo básico#

bash
# Instalar todas las dependencias desde package.json
bun install
 
# Añadir una dependencia
bun add express
 
# Añadir una dependencia de desarrollo
bun add -d vitest
 
# Eliminar una dependencia
bun remove express
 
# Actualizar una dependencia
bun update express
 
# Instalar una versión específica
bun add express@4.18.2

Si has usado npm o yarn, esto es completamente familiar. Las flags son ligeramente diferentes (-d en lugar de --save-dev), pero el modelo mental es idéntico.

La situación del lockfile#

Bun usa bun.lockb, un lockfile binario. Esto es tanto su superpoder como su mayor punto de fricción.

Lo bueno: Es dramáticamente más rápido de leer y escribir. El formato binario significa que Bun puede parsear el lockfile en microsegundos, no los cientos de milisegundos que npm gasta parseando package-lock.json.

Lo malo: No puedes revisarlo en un diff. Si estás en un equipo y alguien actualiza una dependencia, no puedes mirar el diff del lockfile en un PR y ver qué cambió. Esto importa más de lo que los defensores de la velocidad quieren admitir.

bash
# Puedes volcar el lockfile a formato legible
bun bun.lockb > lockfile-dump.txt
 
# O usar la salida de texto integrada
bun install --yarn
# Esto genera un yarn.lock junto a bun.lockb

Mi enfoque: hago commit de bun.lockb al repo y también genero un yarn.lock o package-lock.json como respaldo legible. Cinturón y tirantes.

Soporte de workspaces#

Bun soporta workspaces al estilo npm/yarn:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Instalar dependencias para todos los workspaces
bun install
 
# Ejecutar un script en un workspace específico
bun run --filter packages/shared build
 
# Añadir una dependencia a un workspace específico
bun add react --filter apps/web

El soporte de workspaces es sólido y ha mejorado significativamente. La principal brecha comparada con pnpm es que la resolución de dependencias de workspaces de Bun es menos estricta — la estrictez de pnpm es una característica para monorepos porque detecta dependencias fantasma.

Compatibilidad con proyectos existentes#

Puedes incorporar bun install en casi cualquier proyecto Node.js existente. Lee package.json, respeta .npmrc para la configuración del registro, y maneja peerDependencies correctamente. La transición es típicamente:

bash
# Paso 1: Eliminar lockfile existente y node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# Paso 2: Instalar con Bun
bun install
 
# Paso 3: Verificar que tu app aún funciona
bun run dev
# o: node dist/server.js  (gestor de paquetes Bun, runtime Node)

He hecho esto en una docena de proyectos y he tenido cero problemas con el gestor de paquetes en sí. La única trampa es si tu pipeline de CI busca específicamente package-lock.json — necesitarás actualizarlo para manejar bun.lockb.

Compatibilidad con Node.js#

Esta es la sección donde tengo que ser más cuidadoso, porque la situación cambia cada mes. A principios de 2026, aquí está el panorama honesto.

Qué funciona#

La gran mayoría de los paquetes npm funcionan sin modificación. Bun implementa la mayoría de los módulos integrados de Node.js:

typescript
// Todos estos funcionan como se espera en Bun
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";

Tanto CommonJS como ESM funcionan. require() e import pueden coexistir. TypeScript se ejecuta sin ningún paso de compilación — Bun elimina los tipos en el momento del parseo.

Frameworks que funcionan:

  • Express — funciona, incluyendo el ecosistema de middleware
  • Fastify — funciona
  • Hono — funciona (y es excelente con Bun)
  • Next.js — funciona con salvedades (más sobre esto abajo)
  • Prisma — funciona
  • Drizzle ORM — funciona
  • Socket.io — funciona

Qué no funciona (o tiene problemas)#

Las brechas tienden a caer en algunas categorías:

Addons nativos (node-gyp): Si un paquete usa addons C++ compilados con node-gyp, podría no funcionar con Bun. Bun tiene su propio sistema FFI y soporta muchos módulos nativos, pero la cobertura no es del 100%. Por ejemplo, bcrypt (el nativo) ha tenido problemas — usa bcryptjs en su lugar.

bash
# Verificar si un paquete usa addons nativos
ls node_modules/your-package/binding.gyp  # Si existe, es nativo

Internos específicos de Node.js: Algunos paquetes acceden a internos de Node.js como process.binding() o usan APIs específicas de V8. Estos no funcionarán en Bun ya que ejecuta sobre JavaScriptCore.

typescript
// Esto NO funcionará en Bun — específico de V8
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// Esto SÍ funcionará — usa el equivalente de Bun o un enfoque multiplataforma
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker threads: Bun soporta Web Workers y node:worker_threads, pero hay casos límite. Algunos patrones de uso avanzado — especialmente alrededor de SharedArrayBuffer y Atomics — pueden comportarse diferente.

Módulo vm: node:vm tiene soporte parcial. Si tu código o una dependencia usa vm.createContext() extensivamente (algunos motores de plantillas lo hacen), prueba exhaustivamente.

El rastreador de compatibilidad#

Bun mantiene un rastreador de compatibilidad oficial. Consúltalo antes de comprometerte con Bun para un proyecto:

bash
# Ejecutar la verificación de compatibilidad integrada de Bun en tu proyecto
bun --bun node_modules/.bin/your-tool
 
# La flag --bun fuerza el runtime de Bun incluso para scripts de node_modules

Mi recomendación: no asumas compatibilidad. Ejecuta tu suite de tests bajo Bun antes de decidir. Toma cinco minutos y ahorra horas de depuración.

bash
# Verificación rápida de compatibilidad — ejecuta tu suite de tests completa bajo Bun
bun test  # Si usas el test runner de bun
# o
bun run vitest  # Si usas vitest

APIs integradas de Bun#

Aquí es donde Bun se pone interesante. En lugar de simplemente reimplementar las APIs de Node.js, Bun proporciona sus propias APIs diseñadas para ser más simples y rápidas.

Bun.serve() — El servidor HTTP integrado#

Esta es la API que más uso. Es limpia, rápida, y el soporte de WebSocket viene integrado.

typescript
const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
 
    if (url.pathname === "/") {
      return new Response("Hello from Bun!", {
        headers: { "Content-Type": "text/plain" },
      });
    }
 
    if (url.pathname === "/api/users") {
      const users = [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ];
      return Response.json(users);
    }
 
    return new Response("Not Found", { status: 404 });
  },
});
 
console.log(`Server running at http://localhost:${server.port}`);

Algunas cosas a notar:

  1. Request/Response estándar web — sin API propietaria. El handler fetch recibe un Request estándar y devuelve un Response estándar. Si has escrito un Cloudflare Worker, esto se siente idéntico.
  2. Response.json() — helper integrado para respuestas JSON.
  3. No se necesita importBun.serve es un global. No hay require("http").

Aquí hay un ejemplo más realista con enrutamiento, parseo de body JSON y manejo de errores:

typescript
import { Database } from "bun:sqlite";
 
const db = new Database("app.db");
db.run(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    completed INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
const server = Bun.serve({
  port: process.env.PORT || 3000,
 
  async fetch(req) {
    const url = new URL(req.url);
    const method = req.method;
 
    try {
      // GET /api/todos
      if (url.pathname === "/api/todos" && method === "GET") {
        const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
        return Response.json(todos);
      }
 
      // POST /api/todos
      if (url.pathname === "/api/todos" && method === "POST") {
        const body = await req.json();
 
        if (!body.title || typeof body.title !== "string") {
          return Response.json({ error: "Title is required" }, { status: 400 });
        }
 
        const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
        const todo = stmt.get(body.title);
        return Response.json(todo, { status: 201 });
      }
 
      // DELETE /api/todos/:id
      const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
      if (deleteMatch && method === "DELETE") {
        const id = parseInt(deleteMatch[1], 10);
        db.run("DELETE FROM todos WHERE id = ?", [id]);
        return new Response(null, { status: 204 });
      }
 
      return Response.json({ error: "Not found" }, { status: 404 });
    } catch (error) {
      console.error("Request error:", error);
      return Response.json({ error: "Internal server error" }, { status: 500 });
    }
  },
});
 
console.log(`Server running on port ${server.port}`);

Eso es una API CRUD completa con SQLite en unas 50 líneas. Sin Express, sin ORM, sin cadena de middleware. Para APIs pequeñas y herramientas internas, esta es mi configuración predilecta ahora.

Bun.file() y Bun.write() — I/O de archivos#

La API de archivos de Bun es refrescantemente simple comparada con fs.readFile():

typescript
// Leyendo archivos
const file = Bun.file("./config.json");
const text = await file.text();       // Leer como string
const json = await file.json();       // Parsear como JSON directamente
const bytes = await file.arrayBuffer(); // Leer como ArrayBuffer
const stream = file.stream();          // Leer como ReadableStream
 
// Metadatos del archivo
console.log(file.size);  // Tamaño en bytes
console.log(file.type);  // Tipo MIME (ej., "application/json")
 
// Escribiendo archivos
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
 
// Escribir el body de un Response a un archivo
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

La API Bun.file() es perezosa — no lee el archivo hasta que llamas a .text(), .json(), etc. Esto significa que puedes pasar referencias de Bun.file() sin incurrir en costos de I/O hasta que realmente necesites los datos.

Soporte integrado de WebSocket#

Los WebSockets son ciudadanos de primera clase en Bun.serve():

typescript
const server = Bun.serve({
  port: 3000,
 
  fetch(req, server) {
    const url = new URL(req.url);
 
    if (url.pathname === "/ws") {
      const upgraded = server.upgrade(req, {
        data: {
          userId: url.searchParams.get("userId"),
          joinedAt: Date.now(),
        },
      });
 
      if (!upgraded) {
        return new Response("WebSocket upgrade failed", { status: 400 });
      }
      return undefined;
    }
 
    return new Response("Use /ws for WebSocket connections");
  },
 
  websocket: {
    open(ws) {
      console.log(`Client connected: ${ws.data.userId}`);
      ws.subscribe("chat");
    },
 
    message(ws, message) {
      // Transmitir a todos los suscriptores
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

El patrón server.publish() y ws.subscribe() es pub/sub integrado. Sin Redis, sin librería WebSocket separada. Para funcionalidades en tiempo real simples, esto es increíblemente conveniente.

SQLite integrado con bun:sqlite#

Esto fue lo que más me sorprendió. Bun viene con SQLite integrado directamente en el runtime:

typescript
import { Database } from "bun:sqlite";
 
// Abrir o crear una base de datos
const db = new Database("myapp.db");
 
// Modo WAL para mejor rendimiento de lectura concurrente
db.exec("PRAGMA journal_mode = WAL");
 
// Crear tablas
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
// Sentencias preparadas (reutilizables, más rápidas para consultas repetidas)
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
 
const findByEmail = db.prepare(
  "SELECT * FROM users WHERE email = $email"
);
 
// Uso
const user = insertUser.get({
  $email: "alice@example.com",
  $name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
 
// Transacciones
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
  for (const user of users) {
    insertUser.run({ $email: user.email, $name: user.name });
  }
  return users.length;
});
 
const count = insertMany([
  { email: "bob@example.com", name: "Bob" },
  { email: "carol@example.com", name: "Carol" },
]);
console.log(`Inserted ${count} users`);

Esto es SQLite síncrono con el rendimiento de una librería C (porque lo es — Bun integra libsqlite3 directamente). Para herramientas CLI, apps local-first y servicios pequeños, SQLite integrado significa cero dependencias externas para tu capa de datos.

Test Runner de Bun#

bun test es un reemplazo directo de Jest en la mayoría de los casos. Usa la misma API describe/it/expect y soporta la mayoría de los matchers de Jest.

Uso básico#

typescript
// math.test.ts
import { describe, it, expect } from "bun:test";
 
describe("math utilities", () => {
  it("adds numbers correctly", () => {
    expect(1 + 2).toBe(3);
  });
 
  it("handles floating point", () => {
    expect(0.1 + 0.2).toBeCloseTo(0.3);
  });
});
bash
# Ejecutar todos los tests
bun test
 
# Ejecutar un archivo específico
bun test math.test.ts
 
# Ejecutar tests que coincidan con un patrón
bun test --test-name-pattern "adds numbers"
 
# Modo watch
bun test --watch
 
# Cobertura
bun test --coverage

Mocking#

Bun soporta mocking compatible con Jest:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Mockear un módulo
mock.module("./database", () => ({
  query: mock(() => [{ id: 1, name: "Alice" }]),
}));
 
describe("fetchUsers", () => {
  it("returns users from database", async () => {
    const users = await fetchUsers();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe("Alice");
  });
});
 
// Espiar un método de objeto
describe("console", () => {
  it("tracks console.log calls", () => {
    const logSpy = spyOn(console, "log");
    console.log("test message");
    expect(logSpy).toHaveBeenCalledWith("test message");
    logSpy.mockRestore();
  });
});

Bun Test vs. Vitest — Mi comparación honesta#

Uso Vitest para este proyecto (y la mayoría de mis proyectos). Aquí está por qué no he cambiado completamente:

Dónde bun test gana:

  • Velocidad de arranque. bun test empieza a ejecutar tests más rápido de lo que Vitest tarda en terminar de cargar su configuración.
  • Cero configuración. No se necesita vitest.config.ts para configuraciones básicas.
  • TypeScript integrado. Sin paso de transformación.

Dónde Vitest aún gana:

  • Ecosistema: Vitest tiene más plugins, mejor integración con IDE y una comunidad más grande.
  • Configuración: El sistema de configuración de Vitest es más flexible. Reporters personalizados, archivos de setup complejos, múltiples entornos de test.
  • Modo navegador: Vitest puede ejecutar tests en un navegador real. Bun no.
  • Compatibilidad: Algunas librerías de testing (Testing Library, MSW) han sido probadas más exhaustivamente con Vitest/Jest.
  • Testing de snapshots: Ambos lo soportan, pero la implementación de Vitest es más madura con mejor salida de diff.

Para un proyecto nuevo con necesidades de testing simples, usaría bun test. Para un proyecto establecido con Testing Library, MSW y mocking complejo, me quedo con Vitest.

Bundler de Bun#

bun build es un bundler rápido de JavaScript/TypeScript. No es un reemplazo de webpack — está más en la categoría de esbuild: rápido, con opiniones definidas y enfocado en los casos comunes.

Empaquetado básico#

bash
# Empaquetar un solo punto de entrada
bun build ./src/index.ts --outdir ./dist
 
# Empaquetar para diferentes objetivos
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
 
# Minificar
bun build ./src/index.ts --outdir ./dist --minify
 
# Generar sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap external

API programática#

typescript
const result = await Bun.build({
  entrypoints: ["./src/index.ts", "./src/worker.ts"],
  outdir: "./dist",
  target: "browser",
  minify: {
    whitespace: true,
    identifiers: true,
    syntax: true,
  },
  splitting: true,    // Code splitting
  sourcemap: "external",
  external: ["react", "react-dom"],  // No empaquetar estos
  naming: "[dir]/[name]-[hash].[ext]",
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
});
 
if (!result.success) {
  console.error("Build failed:");
  for (const log of result.logs) {
    console.error(log);
  }
  process.exit(1);
}
 
for (const output of result.outputs) {
  console.log(`${output.path} — ${output.size} bytes`);
}

Tree-Shaking#

Bun soporta tree-shaking para ESM:

typescript
// utils.ts
export function used() {
  return "I'll be in the bundle";
}
 
export function unused() {
  return "I'll be tree-shaken away";
}
 
// index.ts
import { used } from "./utils";
console.log(used());
bash
bun build ./src/index.ts --outdir ./dist --minify
# La función `unused` no aparecerá en la salida

Dónde Bun Build se queda corto#

  • Sin empaquetado de CSS — Necesitas una herramienta separada para CSS (PostCSS, Lightning CSS, Tailwind CLI).
  • Sin generación de HTML — Empaqueta JavaScript/TypeScript, no aplicaciones web completas.
  • Ecosistema de plugins — esbuild tiene un ecosistema de plugins mucho más grande. La API de plugins de Bun es compatible pero la comunidad es más pequeña.
  • Code splitting avanzado — Webpack y Rollup aún ofrecen estrategias de chunks más sofisticadas.

Para construir una librería o el bundle JS de una app web simple, bun build es excelente. Para builds de apps complejas con módulos CSS, optimización de imágenes y estrategias de chunks personalizadas, aún querrás un bundler completo.

Macros de Bun#

Una característica genuinamente única: ejecución de código en tiempo de compilación mediante macros.

typescript
// build-info.ts — este archivo se ejecuta en TIEMPO DE BUILD, no en runtime
export function getBuildInfo() {
  return {
    builtAt: new Date().toISOString(),
    gitSha: require("child_process")
      .execSync("git rev-parse --short HEAD")
      .toString()
      .trim(),
    nodeVersion: process.version,
  };
}
typescript
// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
 
// getBuildInfo() se ejecuta en tiempo de empaquetado
// El resultado se incrusta como un valor estático
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

Después del empaquetado, getBuildInfo() se reemplaza con el objeto literal — sin llamada a función en runtime, sin import de child_process. El código se ejecutó durante el build y el resultado se incrustó. Esto es poderoso para incrustar metadatos de build, feature flags o configuración específica del entorno.

Usando Bun con Next.js#

Esta es la pregunta que más me hacen, así que permíteme ser muy específico.

Qué funciona hoy#

Bun como gestor de paquetes para Next.js — funciona perfectamente:

bash
# Usar Bun para instalar dependencias, luego usar Node.js para ejecutar Next.js
bun install
bun run dev    # Esto realmente ejecuta el script "dev" via Node.js por defecto
bun run build
bun run start

Esto es lo que hago para cada proyecto Next.js. El comando bun run <script> lee la sección scripts de package.json y lo ejecuta. Por defecto, usa el Node.js del sistema para la ejecución real. Obtienes la instalación rápida de paquetes de Bun sin cambiar tu runtime.

Runtime de Bun para desarrollo con Next.js:

bash
# Forzar a Next.js a ejecutarse bajo el runtime de Bun
bun --bun run dev

Esto funciona para desarrollo en la mayoría de los casos. La flag --bun le dice a Bun que use su propio runtime en lugar de delegar a Node.js. El hot module replacement funciona. Las API routes funcionan. Los server components funcionan.

Qué es aún experimental#

Runtime de Bun para builds de producción de Next.js:

bash
# Build con runtime de Bun
bun --bun run build
 
# Iniciar servidor de producción con runtime de Bun
bun --bun run start

Esto funciona para muchos proyectos pero he encontrado casos límite:

  1. Algunos comportamientos del middleware difieren — Si usas middleware de Next.js que depende de APIs específicas de Node.js, podrías encontrar problemas de compatibilidad.
  2. Optimización de imágenes — El pipeline de optimización de imágenes de Next.js usa sharp, que es un addon nativo. Funciona con Bun, pero he visto problemas ocasionales.
  3. ISR (Incremental Static Regeneration) — Funciona, pero no lo he probado bajo estrés con Bun en producción.

Mi recomendación para Next.js#

Usa Bun como gestor de paquetes. Usa Node.js como runtime. Esto te da los beneficios de velocidad de bun install sin ningún riesgo de compatibilidad.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Flujo de trabajo diario
bun install      # Instalación rápida de paquetes
bun run dev      # Ejecuta "next dev" via Node.js
bun run build    # Ejecuta "next build" via Node.js

Cuando la compatibilidad de Bun con Node.js alcance el 100% para el uso interno de Next.js (está cerca, pero aún no llega), cambiaré. Hasta entonces, solo el gestor de paquetes me ahorra suficiente tiempo para justificar la instalación.

Docker con Bun#

La imagen Docker oficial de Bun está bien mantenida y lista para producción.

Dockerfile básico#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# Instalar dependencias
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# Build (si es necesario)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
 
# Producción
FROM base AS production
WORKDIR /app
 
# No ejecutar como root
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
 
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]

Build multi-stage para imagen mínima#

dockerfile
# Etapa de build: imagen completa de Bun con todas las dependencias
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
 
# Etapa de runtime: imagen base más pequeña
FROM oven/bun:1-slim AS runtime
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/dist ./dist
 
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]

Compilación a un solo binario#

Esta es una de las funcionalidades estrella de Bun para despliegue:

bash
# Compilar tu app en un solo ejecutable
bun build --compile ./src/server.ts --outfile server
 
# La salida es un binario independiente — no se necesita Bun ni Node.js para ejecutarlo
./server
dockerfile
# Imagen Docker ultra-mínima usando binario compilado
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
 
# Imagen final — solo el binario
FROM debian:bookworm-slim
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/server ./server
 
EXPOSE 3000
CMD ["./server"]

El binario compilado suele pesar 50-90 MB (incluye el runtime de Bun). Es más grande que un binario de Go pero mucho más pequeño que una instalación completa de Node.js más node_modules. Para despliegues containerizados, la naturaleza autocontenida es una simplificación significativa.

Comparación de tamaño#

bash
# Imagen Node.js
docker images | grep node
# node:20-slim    ~180MB
 
# Imagen Bun
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# Binario compilado sobre debian:bookworm-slim
# ~80MB base + ~70MB binario = ~150MB total
 
# vs. Alpine con Node.js
# node:20-alpine   ~130MB + node_modules

El enfoque de binario elimina node_modules completamente de la imagen final. Sin npm install en producción. Sin superficie de ataque de cadena de suministro de cientos de paquetes. Solo un archivo.

Patrones de migración#

Si estás considerando migrar a Bun, aquí está el camino incremental que recomiendo:

Fase 1: Solo gestor de paquetes (riesgo cero)#

bash
# Reemplazar npm/yarn/pnpm con bun install
# Cambiar tu pipeline de CI:
# Antes:
npm ci
 
# Después:
bun install --frozen-lockfile

Sin cambios de código. Sin cambios de runtime. Solo instalaciones más rápidas. Si algo se rompe (no lo hará), revierte eliminando bun.lockb y ejecutando npm install.

Fase 2: Scripts y herramientas#

bash
# Usar bun para ejecutar scripts de desarrollo
bun run dev
bun run lint
bun run format
 
# Usar bun para scripts de ejecución única
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

Aún usando Node.js como runtime para tu aplicación real. Pero los scripts se benefician del arranque más rápido de Bun y su soporte nativo de TypeScript.

Fase 3: Test runner (riesgo medio)#

bash
# Reemplazar vitest/jest con bun test para suites de test simples
bun test
 
# Mantener vitest para configuraciones de test complejas
# (Testing Library, MSW, entornos personalizados)

Ejecuta tu suite de tests completa bajo bun test. Si todo pasa, has eliminado una devDependency. Si algunos tests fallan por compatibilidad, mantén Vitest para esos y usa bun test para el resto.

Fase 4: Runtime para nuevos servicios (riesgo calculado)#

typescript
// Nuevos microservicios o APIs — empezar con Bun desde el día uno
Bun.serve({
  port: 3000,
  fetch(req) {
    // Tu nuevo servicio aquí
  },
});

No migres servicios Node.js existentes al runtime de Bun. En su lugar, escribe nuevos servicios con Bun desde el inicio. Esto limita tu radio de explosión.

Fase 5: Migración de runtime (avanzado)#

bash
# Solo después de pruebas exhaustivas:
# Reemplazar node con bun para servicios existentes
# Antes:
node dist/server.js
 
# Después:
bun dist/server.js

Solo recomiendo esto para servicios con excelente cobertura de tests. Ejecuta tus pruebas de carga bajo Bun antes de cambiar producción.

Variables de entorno y configuración#

Bun maneja archivos .env automáticamente — no se necesita el paquete dotenv:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// Estos están disponibles sin ningún import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Alternativa específica de Bun

Bun carga .env, .env.local, .env.production, etc. automáticamente, siguiendo la misma convención que Next.js. Una dependencia menos en tu package.json.

Manejo de errores y depuración#

La salida de errores de Bun ha mejorado significativamente, pero aún no está tan pulida como la de Node.js en algunos casos:

bash
# Depurador de Bun — funciona con VS Code
bun --inspect run server.ts
 
# inspect-brk de Bun — pausar en la primera línea
bun --inspect-brk run server.ts

Para VS Code, añade esto a tu .vscode/launch.json:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "bun",
      "request": "launch",
      "name": "Debug Bun",
      "program": "${workspaceFolder}/src/server.ts",
      "cwd": "${workspaceFolder}",
      "stopOnEntry": false,
      "watchMode": false
    }
  ]
}

Los stack traces en Bun son generalmente precisos e incluyen source maps para TypeScript. La principal brecha en depuración es que algunas herramientas específicas de Node.js (como ndb o clinic.js) no funcionan con Bun.

Consideraciones de seguridad#

Algunas cosas a considerar si estás evaluando Bun para producción:

Madurez: Node.js ha estado en producción por más de 15 años. Cada caso límite en parseo HTTP, manejo de TLS y procesamiento de streams ha sido encontrado y corregido. Bun es más joven. Está bien probado, pero la superficie para bugs no descubiertos es más grande.

Parches de seguridad: El equipo de Bun envía actualizaciones frecuentemente, pero el equipo de seguridad de Node.js tiene un proceso formal de CVE, divulgación coordinada y un historial más largo. Para aplicaciones críticas en seguridad, esto importa.

Cadena de suministro: Las funcionalidades integradas de Bun (SQLite, servidor HTTP, WebSockets) significan menos dependencias npm. Menos dependencias significa una superficie de ataque de cadena de suministro más pequeña. Esta es una ventaja genuina de seguridad.

bash
# Comparar conteo de dependencias
# Un proyecto típico de Express + SQLite + WebSocket:
npm ls --all | wc -l
# ~340 paquetes
 
# La misma funcionalidad con las funcionalidades integradas de Bun:
bun pm ls --all | wc -l
# ~12 paquetes (solo el código de tu aplicación)

Esa es una reducción significativa en el número de paquetes en los que confías para tu carga de trabajo de producción.

Ajuste de rendimiento#

Algunos consejos específicos de rendimiento para Bun:

typescript
// Usar opciones de Bun.serve() para ajuste en producción
Bun.serve({
  port: 3000,
 
  // Aumentar tamaño máximo del body de request (por defecto es 128MB)
  maxRequestBodySize: 1024 * 1024 * 50, // 50MB
 
  // Habilitar modo desarrollo para mejores páginas de error
  development: process.env.NODE_ENV !== "production",
 
  // Reutilizar puerto (útil para reinicios sin downtime)
  reusePort: true,
 
  fetch(req) {
    return new Response("OK");
  },
});
typescript
// Usar Bun.Transpiler para transformación de código en runtime
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  target: "browser",
});
 
const code = transpiler.transformSync(`
  const App: React.FC = () => <div>Hello</div>;
  export default App;
`);
bash
# Flags de uso de memoria de Bun
bun --smol run server.ts  # Reducir huella de memoria (ligeramente más lento)
 
# Establecer tamaño máximo del heap
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512MB límite

Trampas comunes#

Después de un año usando Bun, estas son las cosas que me han hecho tropezar:

1. El comportamiento del Fetch global difiere#

typescript
// El fetch de Node.js 18+ y el fetch de Bun son ligeramente diferentes
// en cómo manejan ciertos headers y redirecciones
 
// Bun sigue redirecciones por defecto (como los navegadores)
// El fetch de Node.js también sigue redirecciones, pero el comportamiento
// con ciertos códigos de estado (303, 307, 308) puede diferir
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // Ser explícito sobre el manejo de redirecciones
});

2. Comportamiento de salida del proceso#

typescript
// Bun sale cuando el event loop está vacío
// Node.js a veces sigue ejecutándose debido a handles persistentes
 
// Si tu script de Bun sale inesperadamente, algo no está
// manteniendo vivo el event loop
 
// Esto saldrá inmediatamente en Bun:
setTimeout(() => {}, 0);
 
// Esto seguirá ejecutándose:
setTimeout(() => {}, 1000);
// (Bun sale después de que el timeout se dispare)

3. Configuración de TypeScript#

typescript
// Bun tiene sus propios valores por defecto de tsconfig
// Si estás compartiendo un proyecto entre Bun y Node.js,
// sé explícito en tu tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Añadir definiciones de tipos de Bun
  }
}
bash
# Instalar tipos de Bun
bun add -d @types/bun

4. Hot Reload en desarrollo#

bash
# Bun tiene modo watch integrado
bun --watch run server.ts
 
# Esto reinicia el proceso cuando cambian los archivos
# No es HMR (Hot Module Replacement) — es un reinicio completo
# Pero como Bun arranca tan rápido, se siente instantáneo

5. El archivo de configuración bunfig.toml#

toml
# bunfig.toml — archivo de configuración de Bun (opcional)
 
[install]
# Usar un registro privado
registry = "https://npm.mycompany.com"
 
# Registros por scope
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# Configuración de tests
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# Shell a usar para bun run
shell = "bash"

Mi veredicto#

Después de un año de uso en producción, aquí es donde me he posicionado:

Dónde uso Bun hoy#

Gestor de paquetes para todos los proyectos — incluyendo este blog Next.js. bun install es más rápido, y la compatibilidad es esencialmente perfecta. No veo razón para usar npm o yarn ya. pnpm es la única alternativa que consideraría (por su resolución estricta de dependencias en monorepos).

Runtime para scripts y herramientas CLI — Cualquier archivo TypeScript que necesito ejecutar una vez, lo ejecuto con bun. Sin paso de compilación. Arranque rápido. Carga integrada de .env. Ha reemplazado a ts-node y tsx en mi flujo de trabajo completamente.

Runtime para APIs pequeñas y herramientas internasBun.serve() + bun:sqlite es un stack increíblemente productivo para herramientas internas, manejadores de webhooks y servicios pequeños. El modelo de despliegue "un binario, sin dependencias" es convincente.

Test runner para proyectos simples — Para proyectos con necesidades de testing simples, bun test es rápido y no requiere configuración.

Dónde me quedo con Node.js#

Next.js en producción — No porque Bun no funcione, sino porque la relación riesgo-beneficio no lo justifica aún. Next.js es un framework complejo con muchos puntos de integración. Quiero el runtime más probado en batalla bajo él.

Servicios críticos de producción — Mis servidores API principales ejecutan Node.js detrás de PM2. El ecosistema de monitoreo, las herramientas de depuración, el conocimiento operativo — todo es Node.js. Bun llegará ahí, pero aún no está.

Cualquier cosa con addons nativos — Si una cadena de dependencias incluye addons nativos C++, ni siquiera intento Bun. No vale la pena depurar los problemas de compatibilidad.

Equipos que no están familiarizados con Bun — Introducir Bun como runtime a un equipo que nunca lo ha usado añade overhead cognitivo. Como gestor de paquetes, bien. Como runtime, espera hasta que el equipo esté listo.

Qué estoy observando#

El rastreador de compatibilidad de Bun — Cuando alcance el 100% para las APIs de Node.js que me importan, reevaluaré.

Soporte de frameworks — Next.js, Remix y SvelteKit todos tienen niveles variables de soporte para Bun. Cuando uno de ellos soporte oficialmente Bun como runtime de producción, esa es una señal.

Adopción empresarial — Una vez que empresas con SLAs reales estén ejecutando Bun en producción y escribiendo sobre ello, la pregunta de madurez estará respondida.

La línea de versiones 1.2+ — Bun se mueve rápido. Funcionalidades llegan cada semana. El Bun que uso hoy es significativamente mejor que el Bun que probé hace un año.

Conclusión#

Bun no es una bala de plata. No hará que una app lenta sea rápida y no hará que una API mal diseñada esté bien diseñada. Pero es una mejora genuina en la experiencia de desarrollo para el ecosistema JavaScript.

Lo que más aprecio de Bun no es ninguna funcionalidad individual. Es la reducción en la complejidad de la cadena de herramientas. Un solo binario que instala paquetes, ejecuta TypeScript, empaqueta código y ejecuta tests. Sin tsconfig.json para scripts. Sin Babel. Sin configuración separada de test runner. Solo bun run tu-archivo.ts y funciona.

El consejo práctico: empieza con bun install. Es riesgo cero, beneficio inmediato. Luego prueba bun run para scripts. Después evalúa el resto basándote en tus necesidades específicas. No tienes que ir con todo. Bun funciona perfectamente bien como reemplazo parcial, y probablemente así es como la mayoría de la gente debería usarlo hoy.

El panorama de runtimes de JavaScript es mejor con Bun en él. La competencia está haciendo que Node.js también sea mejor — Node.js 22+ se ha vuelto significativamente más rápido, en parte como respuesta a la presión de Bun. Todos ganan.

Artículos relacionados