Ir para o conteúdo
28 min de leitura

Bun em Produção: O Que Funciona, O Que Não Funciona e O Que Me Surpreendeu

Bun como runtime, gerenciador de pacotes, bundler e test runner. Benchmarks reais, lacunas de compatibilidade com Node.js, padrões de migração e onde eu uso Bun em produção hoje.

Compartilhar:X / TwitterLinkedIn

A cada poucos anos, o ecossistema JavaScript ganha um novo runtime e o discurso segue um arco previsível. Hype. Benchmarks. "X morreu." Verificação de realidade. Acomodação nos casos de uso reais onde a nova ferramenta genuinamente se destaca.

O Bun está no meio desse arco agora. E ao contrário da maioria dos desafiantes, ele está se mantendo. Não porque é "mais rápido" (embora muitas vezes seja), mas porque está resolvendo um problema genuinamente diferente: a toolchain do JavaScript tem peças demais, e o Bun as condensa em uma só.

Eu tenho usado o Bun em várias capacidades há mais de um ano. Parte disso em produção. Parte substituindo ferramentas que eu achava que nunca substituiria. Este post é um relato honesto do que funciona, do que não funciona e onde as lacunas ainda importam.

O Que o Bun Realmente É#

O primeiro equívoco a esclarecer: Bun não é "um Node.js mais rápido." Essa perspectiva o subestima.

O Bun é quatro ferramentas em um único binário:

  1. Um runtime JavaScript/TypeScript — executa seu código, como Node.js ou Deno
  2. Um gerenciador de pacotes — substitui npm, yarn ou pnpm
  3. Um bundler — substitui esbuild, webpack ou Rollup para certos casos de uso
  4. Um test runner — substitui Jest ou Vitest para a maioria das suítes de teste

A principal diferença arquitetural em relação ao Node.js é o motor. O Node.js usa V8 (motor do Chrome). O Bun usa JavaScriptCore (motor do Safari). Ambos são motores maduros e prontos para produção, mas fazem compromissos diferentes. O JavaScriptCore tende a ter tempos de inicialização mais rápidos e menor consumo de memória. O V8 tende a ter melhor throughput máximo para computações de longa duração. Na prática, essas diferenças são menores do que você imagina para a maioria das cargas de trabalho.

O outro grande diferencial: o Bun é escrito em Zig, uma linguagem de programação de sistemas que está aproximadamente no mesmo nível que C, mas com melhores garantias de segurança de memória. É por isso que o Bun pode ser tão agressivo com performance — Zig dá o tipo de controle de baixo nível que C fornece sem a densidade de armadilhas do C.

bash
# Verificar sua versão do Bun
bun --version
 
# Executar um arquivo TypeScript diretamente — sem tsconfig, sem etapa de compilação
bun run server.ts
 
# Instalar pacotes
bun install
 
# Executar testes
bun test
 
# Gerar bundle para produção
bun build ./src/index.ts --outdir ./dist

Isso é um único binário fazendo o trabalho de node + npm + esbuild + vitest. Goste ou não, é uma redução convincente de complexidade.

As Alegações de Velocidade — Benchmarks Honestos#

Deixe-me ser direto sobre isso: os benchmarks de marketing do Bun são selecionados a dedo. Não fraudulentos — selecionados a dedo. Eles mostram os cenários onde o Bun performa melhor, que é exatamente o que você esperaria de material de marketing. O problema é que as pessoas extrapolam desses benchmarks para afirmar que o Bun é "25x mais rápido" em tudo, o que absolutamente não é.

Aqui está onde o Bun é genuinamente, significativamente mais rápido:

Tempo de Inicialização#

Esta é a maior vantagem genuína do Bun e não está nem perto.

bash
# Medindo tempo de inicialização — executar cada um 100 vezes
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Resultados típicos:
# node:  ~40ms
# bun:   ~6ms

Isso é aproximadamente uma diferença de 6-7x no tempo de inicialização. Para scripts, ferramentas CLI e funções serverless onde o cold start importa, isso é significativo. Para um processo de servidor de longa duração que inicia uma vez e roda por semanas, é irrelevante.

Instalação de Pacotes#

Esta é a outra área onde o Bun envergonha a concorrência.

bash
# Benchmark de instalação limpa — deletar node_modules e lockfile primeiro
rm -rf node_modules bun.lockb package-lock.json
 
# Tempo do npm
time npm install
# Real: ~18.4s (projeto de tamanho médio típico)
 
# Tempo do bun
time bun install
# Real: ~2.1s

Isso é uma diferença de 8-9x, e é consistente. As razões são principalmente:

  1. Lockfile bináriobun.lockb é um formato binário, não JSON. Mais rápido para ler e escrever.
  2. Cache global — O Bun mantém um cache global de módulos para que reinstalações entre projetos compartilhem pacotes baixados.
  3. I/O do Zig — O gerenciador de pacotes em si é escrito em Zig, não JavaScript. Operações de I/O de arquivo são mais próximas do metal.
  4. Estratégia de symlink — O Bun usa hardlinks do cache global em vez de copiar arquivos.

Throughput do Servidor HTTP#

O servidor HTTP embutido do Bun é rápido, mas as comparações precisam de contexto.

bash
# Benchmark rápido e simples com bombardier
# Testando uma resposta simples "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 respostas triviais. Bun vs. Express: aproximadamente 7x, mas isso é injusto porque o Express adiciona overhead de middleware. No momento que você adiciona lógica real — consultas ao banco de dados, autenticação, serialização JSON de dados reais — a diferença diminui drasticamente.

Onde a Diferença É Insignificante#

Computação CPU-bound:

typescript
// fibonacci.ts — isso é limitado pelo motor, não pelo 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

O Node.js (V8) na verdade vence ligeiramente aqui. O compilador JIT do V8 é mais agressivo em loops quentes. Para trabalho CPU-bound, as diferenças do motor são praticamente as mesmas — às vezes o V8 vence, às vezes o JSC vence, e as diferenças estão dentro do ruído.

Como Executar Seus Próprios Benchmarks#

Não confie nos benchmarks de ninguém, incluindo os meus. Veja como medir o que importa para sua carga de trabalho específica:

bash
# Instalar hyperfine para benchmarking adequado
brew install hyperfine  # macOS
# ou: cargo install hyperfine
 
# Benchmark de inicialização + execução da sua aplicação real
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# Para servidores HTTP, use bombardier ou wrk
# Importante: teste com payloads realistas, não "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# Comparação de memória
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

A regra geral: se seu gargalo é I/O (sistema de arquivos, rede, banco de dados), a vantagem do Bun é modesta. Se seu gargalo é tempo de inicialização ou velocidade da toolchain, o Bun vence fácil. Se seu gargalo é computação pura, é um empate.

Bun como Gerenciador de Pacotes#

É aqui que eu mudei completamente. Mesmo em projetos onde eu rodo Node.js em produção, eu uso bun install para desenvolvimento local e CI. É simplesmente mais rápido, e a compatibilidade é excelente.

O Básico#

bash
# Instalar todas as dependências do package.json
bun install
 
# Adicionar uma dependência
bun add express
 
# Adicionar uma dependência de desenvolvimento
bun add -d vitest
 
# Remover uma dependência
bun remove express
 
# Atualizar uma dependência
bun update express
 
# Instalar uma versão específica
bun add express@4.18.2

Se você já usou npm ou yarn, isso é totalmente familiar. As flags são ligeiramente diferentes (-d em vez de --save-dev), mas o modelo mental é idêntico.

A Situação do Lockfile#

O Bun usa bun.lockb, um lockfile binário. Isso é tanto seu superpoder quanto seu maior ponto de fricção.

O bom: É dramaticamente mais rápido para ler e escrever. O formato binário significa que o Bun pode analisar o lockfile em microssegundos, não nas centenas de milissegundos que o npm gasta analisando package-lock.json.

O ruim: Você não consegue revisá-lo em um diff. Se você está em um time e alguém atualiza uma dependência, você não consegue olhar o diff do lockfile em um PR e ver o que mudou. Isso importa mais do que os defensores da velocidade querem admitir.

bash
# Você pode fazer dump do lockfile em formato legível
bun bun.lockb > lockfile-dump.txt
 
# Ou usar a saída em texto embutida
bun install --yarn
# Isso gera um yarn.lock junto com bun.lockb

Minha abordagem: eu commito bun.lockb no repo e também gero um yarn.lock ou package-lock.json como fallback legível. Cinto e suspensórios.

Suporte a Workspaces#

O Bun suporta workspaces no estilo npm/yarn:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Instalar dependências para todos os workspaces
bun install
 
# Executar um script em um workspace específico
bun run --filter packages/shared build
 
# Adicionar uma dependência a um workspace específico
bun add react --filter apps/web

O suporte a workspaces é sólido e melhorou significativamente. A principal lacuna comparada ao pnpm é que a resolução de dependências de workspace do Bun é menos estrita — a rigidez do pnpm é uma feature para monorepos porque detecta dependências fantasma.

Compatibilidade com Projetos Existentes#

Você pode substituir bun install em praticamente qualquer projeto Node.js existente. Ele lê package.json, respeita .npmrc para configuração de registry e lida com peerDependencies corretamente. A transição é tipicamente:

bash
# Passo 1: Deletar lockfile existente e node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# Passo 2: Instalar com Bun
bun install
 
# Passo 3: Verificar se sua aplicação ainda funciona
bun run dev
# ou: node dist/server.js  (gerenciador de pacotes Bun, runtime Node)

Eu fiz isso em uma dúzia de projetos e não tive nenhum problema com o gerenciador de pacotes em si. O único ponto de atenção é se seu pipeline de CI busca especificamente package-lock.json — você precisará atualizá-lo para lidar com bun.lockb.

Compatibilidade com Node.js#

Esta é a seção onde eu preciso ser mais cuidadoso, porque a situação muda a cada mês. No início de 2026, aqui está o panorama honesto.

O Que Funciona#

A grande maioria dos pacotes npm funciona sem modificação. O Bun implementa a maioria dos módulos embutidos do Node.js:

typescript
// Todos estes funcionam como esperado no 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 quanto ESM funcionam. require() e import podem coexistir. TypeScript roda sem nenhuma etapa de compilação — o Bun remove os tipos no momento da análise.

Frameworks que funcionam:

  • Express — funciona, incluindo ecossistema de middleware
  • Fastify — funciona
  • Hono — funciona (e é excelente com Bun)
  • Next.js — funciona com ressalvas (mais detalhes abaixo)
  • Prisma — funciona
  • Drizzle ORM — funciona
  • Socket.io — funciona

O Que Não Funciona (ou Tem Problemas)#

As lacunas tendem a cair em algumas categorias:

Addons nativos (node-gyp): Se um pacote usa addons C++ compilados com node-gyp, pode não funcionar com o Bun. O Bun tem seu próprio sistema FFI e suporta muitos módulos nativos, mas a cobertura não é 100%. Por exemplo, bcrypt (o nativo) teve problemas — use bcryptjs no lugar.

bash
# Verificar se um pacote usa addons nativos
ls node_modules/your-package/binding.gyp  # Se isto existe, é nativo

Internos específicos do Node.js: Alguns pacotes acessam internos do Node.js como process.binding() ou usam APIs específicas do V8. Esses não funcionarão no Bun já que ele roda no JavaScriptCore.

typescript
// Isso NÃO funcionará no Bun — específico do V8
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// Isso FUNCIONARÁ — use o equivalente do Bun ou uma abordagem cross-runtime
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker threads: O Bun suporta Web Workers e node:worker_threads, mas existem casos extremos. Alguns padrões de uso avançado — especialmente em torno de SharedArrayBuffer e Atomics — podem se comportar de forma diferente.

Módulo vm: node:vm tem suporte parcial. Se seu código ou uma dependência usa vm.createContext() extensivamente (alguns motores de template fazem isso), teste minuciosamente.

O Rastreador de Compatibilidade#

O Bun mantém um rastreador de compatibilidade oficial. Verifique-o antes de se comprometer com o Bun para um projeto:

bash
# Execute a verificação de compatibilidade embutida do Bun no seu projeto
bun --bun node_modules/.bin/your-tool
 
# A flag --bun força o runtime do Bun mesmo para scripts de node_modules

Minha recomendação: não assuma compatibilidade. Execute sua suíte de testes sob o Bun antes de decidir. Leva cinco minutos e economiza horas de depuração.

bash
# Verificação rápida de compatibilidade — execute toda sua suíte de testes sob o Bun
bun test  # Se você usa bun test runner
# ou
bun run vitest  # Se você usa vitest

APIs Embutidas do Bun#

É aqui que o Bun fica interessante. Em vez de apenas reimplementar APIs do Node.js, o Bun fornece suas próprias APIs projetadas para serem mais simples e rápidas.

Bun.serve() — O Servidor HTTP Embutido#

Esta é a API que eu mais uso. É limpa, rápida, e o suporte a WebSocket é integrado diretamente.

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}`);

Algumas coisas a notar:

  1. Request/Response padrão web — sem API proprietária. O handler fetch recebe um Request padrão e retorna um Response padrão. Se você já escreveu um Cloudflare Worker, isso é idêntico.
  2. Response.json() — helper embutido para resposta JSON.
  3. Sem import necessárioBun.serve é global. Sem require("http").

Aqui está um exemplo mais realista com roteamento, parsing de body JSON e tratamento de erros:

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}`);

Essa é uma API CRUD completa com SQLite em aproximadamente 50 linhas. Sem Express, sem ORM, sem cadeia de middleware. Para APIs pequenas e ferramentas internas, esse é meu setup padrão agora.

Bun.file() e Bun.write() — I/O de Arquivo#

A API de arquivo do Bun é refrescantemente simples comparada a fs.readFile():

typescript
// Lendo arquivos
const file = Bun.file("./config.json");
const text = await file.text();       // Ler como string
const json = await file.json();       // Fazer parse como JSON diretamente
const bytes = await file.arrayBuffer(); // Ler como ArrayBuffer
const stream = file.stream();          // Ler como ReadableStream
 
// Metadados do arquivo
console.log(file.size);  // Tamanho em bytes
console.log(file.type);  // Tipo MIME (ex.: "application/json")
 
// Escrevendo arquivos
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"));
 
// Escrever o body de uma Response em um arquivo
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

A API Bun.file() é lazy — ela não lê o arquivo até você chamar .text(), .json(), etc. Isso significa que você pode passar referências de Bun.file() sem incorrer em custos de I/O até que você realmente precise dos dados.

Suporte WebSocket Embutido#

WebSockets são cidadãos de primeira classe em 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) {
      // Broadcast para todos os inscritos
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

O padrão server.publish() e ws.subscribe() é pub/sub embutido. Sem Redis, sem biblioteca WebSocket separada. Para funcionalidades real-time simples, isso é incrivelmente conveniente.

SQLite Embutido com bun:sqlite#

Isso foi o que mais me surpreendeu. O Bun vem com SQLite integrado diretamente no runtime:

typescript
import { Database } from "bun:sqlite";
 
// Abrir ou criar um banco de dados
const db = new Database("myapp.db");
 
// Modo WAL para melhor performance de leitura concorrente
db.exec("PRAGMA journal_mode = WAL");
 
// Criar tabelas
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'))
  )
`);
 
// Prepared statements (reutilizáveis, mais rápidos 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", ... }
 
// Transações
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`);

Isso é SQLite síncrono com a performance de uma biblioteca C (porque é uma — o Bun embute libsqlite3 diretamente). Para ferramentas CLI, apps local-first e serviços pequenos, SQLite embutido significa zero dependências externas para sua camada de dados.

Test Runner do Bun#

bun test é um substituto direto para o Jest na maioria dos casos. Ele usa a mesma API describe/it/expect e suporta a maioria dos matchers do 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
# Executar todos os testes
bun test
 
# Executar arquivo específico
bun test math.test.ts
 
# Executar testes que correspondam a um padrão
bun test --test-name-pattern "adds numbers"
 
# Modo watch
bun test --watch
 
# Cobertura
bun test --coverage

Mocking#

O Bun suporta mocking compatível com Jest:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Mock de um 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");
  });
});
 
// Espionar um 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 — Minha Comparação Honesta#

Eu uso Vitest para este projeto (e a maioria dos meus projetos). Eis por que eu não mudei completamente:

Onde bun test vence:

  • Velocidade de inicialização. bun test começa a executar testes mais rápido do que o Vitest consegue terminar de carregar sua configuração.
  • Zero configuração. Nenhum vitest.config.ts necessário para setups básicos.
  • TypeScript embutido. Sem etapa de transformação.

Onde Vitest ainda vence:

  • Ecossistema: Vitest tem mais plugins, melhor integração com IDE e uma comunidade maior.
  • Configuração: O sistema de configuração do Vitest é mais flexível. Reporters customizados, arquivos de setup complexos, múltiplos ambientes de teste.
  • Modo browser: Vitest pode executar testes em um navegador real. Bun não pode.
  • Compatibilidade: Algumas bibliotecas de teste (Testing Library, MSW) foram testadas mais minuciosamente com Vitest/Jest.
  • Teste de snapshots: Ambos suportam, mas a implementação do Vitest é mais madura com melhor saída de diff.

Para um novo projeto com necessidades de teste simples, eu usaria bun test. Para um projeto estabelecido com Testing Library, MSW e mocking complexo, eu mantenho o Vitest.

Bundler do Bun#

bun build é um bundler JavaScript/TypeScript rápido. Não é um substituto do webpack — está mais na categoria do esbuild: rápido, opinativo e focado nos casos comuns.

Bundling Básico#

bash
# Gerar bundle de um único entry point
bun build ./src/index.ts --outdir ./dist
 
# Gerar bundle para diferentes alvos
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
 
# Gerar 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"],  // Não incluir no bundle
  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#

O Bun suporta 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
# A função `unused` não aparecerá na saída

Onde o Bun Build Fica Aquém#

  • Sem bundling de CSS — Você precisa de uma ferramenta separada para CSS (PostCSS, Lightning CSS, Tailwind CLI).
  • Sem geração de HTML — Ele faz bundle de JavaScript/TypeScript, não de aplicações web completas.
  • Ecossistema de plugins — esbuild tem um ecossistema de plugins muito maior. A API de plugins do Bun é compatível, mas a comunidade é menor.
  • Code splitting avançado — Webpack e Rollup ainda oferecem estratégias de chunks mais sofisticadas.

Para construir uma biblioteca ou o bundle JS de uma aplicação web simples, bun build é excelente. Para builds complexos de aplicações com CSS modules, otimização de imagens e estratégias de chunks customizadas, você ainda vai querer um bundler completo.

Macros do Bun#

Uma funcionalidade genuinamente única: execução de código em tempo de compilação via macros.

typescript
// build-info.ts — este arquivo roda em TEMPO DE BUILD, não em 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() executa em tempo de bundle
// O resultado é inlined como um valor estático
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

Após o bundling, getBuildInfo() é substituído pelo objeto literal — sem chamada de função em runtime, sem import de child_process. O código rodou durante o build e o resultado foi inlined. Isso é poderoso para embutir metadados de build, feature flags ou configuração específica de ambiente.

Usando Bun com Next.js#

Esta é a pergunta que eu mais recebo, então deixe-me ser bem específico.

O Que Funciona Hoje#

Bun como gerenciador de pacotes para Next.js — funciona perfeitamente:

bash
# Usar Bun para instalar dependências, depois usar Node.js para rodar Next.js
bun install
bun run dev    # Isso na verdade roda o script "dev" via Node.js por padrão
bun run build
bun run start

É o que eu faço para todo projeto Next.js. O comando bun run <script> lê a seção scripts do package.json e executa. Por padrão, ele usa o Node.js do sistema para a execução real. Você obtém a instalação rápida de pacotes do Bun sem mudar seu runtime.

Runtime Bun para desenvolvimento com Next.js:

bash
# Forçar Next.js a rodar sob o runtime do Bun
bun --bun run dev

Isso funciona para desenvolvimento na maioria dos casos. A flag --bun diz ao Bun para usar seu próprio runtime em vez de delegar ao Node.js. Hot module replacement funciona. API routes funcionam. Server components funcionam.

O Que Ainda É Experimental#

Runtime Bun para builds de produção do Next.js:

bash
# Build com runtime Bun
bun --bun run build
 
# Iniciar servidor de produção com runtime Bun
bun --bun run start

Isso funciona para muitos projetos, mas eu encontrei casos extremos:

  1. Alguns comportamentos de middleware diferem — Se você usa middleware do Next.js que depende de APIs específicas do Node.js, pode encontrar problemas de compatibilidade.
  2. Otimização de imagens — O pipeline de otimização de imagens do Next.js usa sharp, que é um addon nativo. Funciona com Bun, mas eu vi problemas ocasionais.
  3. ISR (Incremental Static Regeneration) — Funciona, mas eu não fiz stress test sob Bun em produção.

Minha Recomendação para Next.js#

Use Bun como gerenciador de pacotes. Use Node.js como runtime. Isso lhe dá os benefícios de velocidade do bun install sem nenhum risco de compatibilidade.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Fluxo de trabalho diário
bun install      # Instalação rápida de pacotes
bun run dev      # Roda "next dev" via Node.js
bun run build    # Roda "next build" via Node.js

Quando a compatibilidade do Bun com Node.js atingir 100% para o uso interno do Next.js (está perto, mas ainda não chegou), eu vou mudar. Até lá, só o gerenciador de pacotes me economiza tempo suficiente para justificar a instalação.

Docker com Bun#

A imagem Docker oficial do Bun é bem mantida e pronta para produção.

Dockerfile Básico#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# Instalar dependências
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# Build (se necessário)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
 
# Produção
FROM base AS production
WORKDIR /app
 
# Não rodar 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-Estágio para Imagem Mínima#

dockerfile
# Estágio de build: imagem completa do Bun com todas as dependências
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
 
# Estágio de runtime: imagem base menor
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"]

Compilando para um Único Binário#

Esta é uma das funcionalidades matadoras do Bun para deploy:

bash
# Compilar sua aplicação em um executável standalone
bun build --compile ./src/server.ts --outfile server
 
# A saída é um binário standalone — não precisa de Bun ou Node.js para rodar
./server
dockerfile
# Imagem Docker ultra-mínima usando binário 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
 
# Imagem final — apenas o binário
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"]

O binário compilado tipicamente tem 50-90 MB (ele inclui o runtime do Bun). Isso é maior que um binário Go, mas muito menor que uma instalação completa de Node.js mais node_modules. Para deploys containerizados, a natureza autocontida é uma simplificação significativa.

Comparação de Tamanho#

bash
# Imagem Node.js
docker images | grep node
# node:20-slim    ~180MB
 
# Imagem Bun
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# Binário compilado em debian:bookworm-slim
# ~80MB base + ~70MB binário = ~150MB total
 
# vs. Alpine com Node.js
# node:20-alpine   ~130MB + node_modules

A abordagem de binário elimina completamente node_modules da imagem final. Sem npm install em produção. Sem superfície de ataque da cadeia de suprimentos de centenas de pacotes. Apenas um arquivo.

Padrões de Migração#

Se você está considerando migrar para o Bun, aqui está o caminho incremental que eu recomendo:

Fase 1: Apenas Gerenciador de Pacotes (Risco Zero)#

bash
# Substituir npm/yarn/pnpm pelo bun install
# Mude seu pipeline de CI:
# Antes:
npm ci
 
# Depois:
bun install --frozen-lockfile

Sem mudanças de código. Sem mudanças de runtime. Apenas instalações mais rápidas. Se algo quebrar (não vai), reverta deletando bun.lockb e rodando npm install.

Fase 2: Scripts e Tooling#

bash
# Usar bun para executar scripts de desenvolvimento
bun run dev
bun run lint
bun run format
 
# Usar bun para scripts pontuais
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

Ainda usando Node.js como runtime para sua aplicação real. Mas scripts se beneficiam da inicialização mais rápida do Bun e do suporte nativo a TypeScript.

Fase 3: Test Runner (Risco Médio)#

bash
# Substituir vitest/jest pelo bun test para suítes de teste simples
bun test
 
# Manter vitest para setups de teste complexos
# (Testing Library, MSW, ambientes customizados)

Execute toda sua suíte de testes sob bun test. Se tudo passar, você eliminou uma devDependency. Se alguns testes falharem por compatibilidade, mantenha o Vitest para esses e use bun test para o resto.

Fase 4: Runtime para Novos Serviços (Risco Calculado)#

typescript
// Novos microserviços ou APIs — comece com Bun desde o dia um
Bun.serve({
  port: 3000,
  fetch(req) {
    // Seu novo serviço aqui
  },
});

Não migre serviços Node.js existentes para o runtime Bun. Em vez disso, escreva novos serviços com Bun desde o início. Isso limita seu raio de explosão.

Fase 5: Migração de Runtime (Avançado)#

bash
# Somente após testes minuciosos:
# Substituir node pelo bun para serviços existentes
# Antes:
node dist/server.js
 
# Depois:
bun dist/server.js

Eu só recomendo isso para serviços com excelente cobertura de testes. Execute seus testes de carga sob o Bun antes de trocar em produção.

Variáveis de Ambiente e Configuração#

O Bun lida com arquivos .env automaticamente — sem necessidade do pacote dotenv:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// Estes estão disponíveis sem nenhum import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Alternativa específica do Bun

O Bun carrega .env, .env.local, .env.production, etc. automaticamente, seguindo a mesma convenção do Next.js. Uma dependência a menos no seu package.json.

Tratamento de Erros e Depuração#

A saída de erros do Bun melhorou significativamente, mas ainda não é tão polida quanto a do Node.js em alguns casos:

bash
# Debugger do Bun — funciona com VS Code
bun --inspect run server.ts
 
# Inspect-brk do Bun — pausar na primeira linha
bun --inspect-brk run server.ts

Para VS Code, adicione isso ao seu .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
    }
  ]
}

Stack traces no Bun são geralmente precisos e incluem source maps para TypeScript. A principal lacuna de depuração é que algumas ferramentas específicas de Node.js (como ndb ou clinic.js) não funcionam com o Bun.

Considerações de Segurança#

Algumas coisas para pensar se você está avaliando o Bun para produção:

Maturidade: O Node.js está em produção há mais de 15 anos. Cada caso extremo em análise HTTP, tratamento TLS e processamento de streams foi encontrado e corrigido. O Bun é mais jovem. É bem testado, mas a superfície para bugs não descobertos é maior.

Patches de segurança: A equipe do Bun publica atualizações frequentemente, mas a equipe de segurança do Node.js tem um processo formal de CVE, divulgação coordenada e um histórico mais longo. Para aplicações críticas de segurança, isso importa.

Cadeia de suprimentos: As funcionalidades embutidas do Bun (SQLite, servidor HTTP, WebSockets) significam menos dependências npm. Menos dependências significa uma superfície de ataque de cadeia de suprimentos menor. Essa é uma vantagem genuína de segurança.

bash
# Comparar contagem de dependências
# Um projeto típico Express + SQLite + WebSocket:
npm ls --all | wc -l
# ~340 pacotes
 
# A mesma funcionalidade com embutidos do Bun:
bun pm ls --all | wc -l
# ~12 pacotes (apenas seu código de aplicação)

Essa é uma redução significativa no número de pacotes em que você está confiando sua carga de trabalho de produção.

Ajuste de Performance#

Algumas dicas de performance específicas do Bun:

typescript
// Use opções do Bun.serve() para ajuste de produção
Bun.serve({
  port: 3000,
 
  // Aumentar tamanho máximo do body da request (padrão é 128MB)
  maxRequestBodySize: 1024 * 1024 * 50, // 50MB
 
  // Habilitar modo de desenvolvimento para melhores páginas de erro
  development: process.env.NODE_ENV !== "production",
 
  // Reusar porta (útil para reinícios sem downtime)
  reusePort: true,
 
  fetch(req) {
    return new Response("OK");
  },
});
typescript
// Usar Bun.Transpiler para transformação de código em 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 memória do Bun
bun --smol run server.ts  # Reduzir footprint de memória (ligeiramente mais lento)
 
# Definir tamanho máximo do heap
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512MB limite

Armadilhas Comuns#

Após um ano usando o Bun, aqui estão as coisas que me pegaram:

1. Comportamento Global do Fetch Difere#

typescript
// O fetch do Node.js 18+ e o fetch do Bun são ligeiramente diferentes
// em como lidam com certos headers e redirecionamentos
 
// O Bun segue redirecionamentos por padrão (como navegadores)
// O fetch do Node.js também segue redirecionamentos, mas o comportamento
// com certos status codes (303, 307, 308) pode diferir
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // Ser explícito sobre tratamento de redirecionamento
});

2. Comportamento de Saída do Processo#

typescript
// O Bun sai quando o event loop está vazio
// O Node.js às vezes continua rodando devido a handles pendentes
 
// Se seu script Bun sai inesperadamente, algo não está
// mantendo o event loop ativo
 
// Isso vai sair imediatamente no Bun:
setTimeout(() => {}, 0);
 
// Isso vai continuar rodando:
setTimeout(() => {}, 1000);
// (Bun sai após o timeout disparar)

3. Configuração do TypeScript#

typescript
// O Bun tem seus próprios padrões de tsconfig
// Se você está compartilhando um projeto entre Bun e Node.js,
// seja explícito no seu tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Adicionar definições de tipo do Bun
  }
}
bash
# Instalar tipos do Bun
bun add -d @types/bun

4. Hot Reload em Desenvolvimento#

bash
# O Bun tem modo watch embutido
bun --watch run server.ts
 
# Isso reinicia o processo nas mudanças de arquivo
# Não é HMR (Hot Module Replacement) — é um restart completo
# Mas como o Bun inicia tão rápido, parece instantâneo

5. O Arquivo de Configuração bunfig.toml#

toml
# bunfig.toml — arquivo de configuração do Bun (opcional)
 
[install]
# Usar um registry privado
registry = "https://npm.mycompany.com"
 
# Registries com escopo
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# Configuração de testes
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# Shell a usar para bun run
shell = "bash"

Meu Veredito#

Após um ano de uso em produção, aqui é onde eu me posicionei:

Onde Eu Uso Bun Hoje#

Gerenciador de pacotes para todos os projetos — incluindo este blog Next.js. bun install é mais rápido, e a compatibilidade é essencialmente perfeita. Não vejo razão para usar npm ou yarn mais. pnpm é a única alternativa que eu consideraria (pela sua resolução estrita de dependências em monorepos).

Runtime para scripts e ferramentas CLI — Qualquer arquivo TypeScript que eu preciso rodar uma vez, eu rodo com bun. Sem etapa de compilação. Inicialização rápida. Carregamento embutido de .env. Substituiu completamente ts-node e tsx no meu fluxo de trabalho.

Runtime para APIs pequenas e ferramentas internasBun.serve() + bun:sqlite é uma stack incrivelmente produtiva para ferramentas internas, handlers de webhook e serviços pequenos. O modelo de deploy "um binário, sem dependências" é convincente.

Test runner para projetos simples — Para projetos com necessidades de teste simples, bun test é rápido e não requer configuração.

Onde Eu Fico com Node.js#

Next.js em produção — Não porque o Bun não funciona, mas porque o risco-recompensa não se justifica ainda. Next.js é um framework complexo com muitos pontos de integração. Eu quero o runtime mais testado em batalha por baixo dele.

Serviços de produção críticos — Meus servidores de API principais rodam Node.js por trás do PM2. O ecossistema de monitoramento, as ferramentas de depuração, o conhecimento operacional — tudo é Node.js. O Bun vai chegar lá, mas ainda não chegou.

Qualquer coisa com addons nativos — Se uma cadeia de dependências inclui addons nativos C++, eu nem tento o Bun. Não vale a pena depurar os problemas de compatibilidade.

Equipes que não estão familiarizadas com Bun — Introduzir o Bun como runtime para uma equipe que nunca o usou adiciona sobrecarga cognitiva. Como gerenciador de pacotes, tudo bem. Como runtime, espere até que a equipe esteja pronta.

O Que Eu Estou Acompanhando#

O rastreador de compatibilidade do Bun — Quando atingir 100% para as APIs do Node.js que eu me importo, eu vou reavaliar.

Suporte de frameworks — Next.js, Remix e SvelteKit todos têm níveis variados de suporte ao Bun. Quando um deles oficialmente suportar Bun como runtime de produção, isso é um sinal.

Adoção enterprise — Uma vez que empresas com SLAs reais estejam rodando Bun em produção e escrevendo sobre isso, a questão de maturidade estará respondida.

A linha de releases 1.2+ — O Bun está se movendo rápido. Funcionalidades chegam toda semana. O Bun que eu uso hoje é significativamente melhor do que o Bun que eu testei um ano atrás.

Conclusão#

O Bun não é uma bala de prata. Ele não vai tornar uma aplicação lenta rápida e não vai tornar uma API mal projetada bem projetada. Mas é uma melhoria genuína na experiência do desenvolvedor para o ecossistema JavaScript.

A coisa que eu mais aprecio sobre o Bun não é nenhuma funcionalidade individual. É a redução na complexidade da toolchain. Um binário que instala pacotes, roda TypeScript, gera bundles de código e executa testes. Sem tsconfig.json para scripts. Sem Babel. Sem configuração separada de test runner. Apenas bun run your-file.ts e funciona.

O conselho prático: comece com bun install. É risco zero, benefício imediato. Depois tente bun run para scripts. Depois avalie o resto baseado nas suas necessidades específicas. Você não precisa ir com tudo. O Bun funciona perfeitamente bem como substituto parcial, e provavelmente é assim que a maioria das pessoas deveria usá-lo hoje.

O cenário de runtimes JavaScript é melhor com o Bun nele. A competição está tornando o Node.js melhor também — Node.js 22+ ficou significativamente mais rápido, parcialmente em resposta à pressão do Bun. Todo mundo ganha.

Posts Relacionados