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.
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:
- Um runtime JavaScript/TypeScript — executa seu código, como Node.js ou Deno
- Um gerenciador de pacotes — substitui npm, yarn ou pnpm
- Um bundler — substitui esbuild, webpack ou Rollup para certos casos de uso
- 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.
# 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 ./distIsso é 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.
# 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: ~6msIsso é 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.
# 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.1sIsso é uma diferença de 8-9x, e é consistente. As razões são principalmente:
- Lockfile binário —
bun.lockbé um formato binário, não JSON. Mais rápido para ler e escrever. - Cache global — O Bun mantém um cache global de módulos para que reinstalações entre projetos compartilhem pacotes baixados.
- 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.
- 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.
# 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.000Bun 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:
// 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`);bun run fibonacci.ts # ~1650ms
node fibonacci.ts # ~1580msO 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:
# 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 # macOSA 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#
# 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.2Se 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.
# 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.lockbMinha 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:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# 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/webO 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:
# 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:
// 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.
# Verificar se um pacote usa addons nativos
ls node_modules/your-package/binding.gyp # Se isto existe, é nativoInternos 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.
// 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:
# 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_modulesMinha 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.
# 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 vitestAPIs 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.
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:
- Request/Response padrão web — sem API proprietária. O handler
fetchrecebe umRequestpadrão e retorna umResponsepadrão. Se você já escreveu um Cloudflare Worker, isso é idêntico. Response.json()— helper embutido para resposta JSON.- Sem import necessário —
Bun.serveé global. Semrequire("http").
Aqui está um exemplo mais realista com roteamento, parsing de body JSON e tratamento de erros:
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():
// 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():
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:
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#
// 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);
});
});# 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 --coverageMocking#
O Bun suporta mocking compatível com Jest:
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 testcomeça a executar testes mais rápido do que o Vitest consegue terminar de carregar sua configuração. - Zero configuração. Nenhum
vitest.config.tsnecessá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#
# 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 externalAPI Programática#
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:
// 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());bun build ./src/index.ts --outdir ./dist --minify
# A função `unused` não aparecerá na saídaOnde 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.
// 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,
};
}// 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:
# 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:
# Forçar Next.js a rodar sob o runtime do Bun
bun --bun run devIsso 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:
# Build com runtime Bun
bun --bun run build
# Iniciar servidor de produção com runtime Bun
bun --bun run startIsso funciona para muitos projetos, mas eu encontrei casos extremos:
- 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.
- 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.
- 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.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# 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.jsQuando 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#
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#
# 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:
# 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# 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#
# 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_modulesA 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)#
# Substituir npm/yarn/pnpm pelo bun install
# Mude seu pipeline de CI:
# Antes:
npm ci
# Depois:
bun install --frozen-lockfileSem 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#
# 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.tsAinda 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)#
# 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)#
// 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)#
# Somente após testes minuciosos:
# Substituir node pelo bun para serviços existentes
# Antes:
node dist/server.js
# Depois:
bun dist/server.jsEu 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:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// 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 BunO 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:
# 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.tsPara VS Code, adicione isso ao seu .vscode/launch.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.
# 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:
// 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");
},
});// 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;
`);# 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 limiteArmadilhas Comuns#
Após um ano usando o Bun, aqui estão as coisas que me pegaram:
1. Comportamento Global do Fetch Difere#
// 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#
// 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#
// 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
}
}# Instalar tipos do Bun
bun add -d @types/bun4. Hot Reload em Desenvolvimento#
# 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âneo5. O Arquivo de Configuração bunfig.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 internas — Bun.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.