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 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:
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.
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:
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.
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:
bun.lockb é um formato binário, não JSON. Mais rápido para ler e escrever.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.
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.
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.
É 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.
# 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.
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.
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.
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.
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.
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:
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 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 vitestÉ 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.
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:
fetch recebe um Request padrão e retorna um Response padrão. Se você já escreveu um Cloudflare Worker, isso é idêntico.Response.json() — helper embutido para resposta JSON.Bun.serve é global. Sem require("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.
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.
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.
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.
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.
// 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 --coverageO 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();
});
});Eu uso Vitest para este projeto (e a maioria dos meus projetos). Eis por que eu não mudei completamente:
Onde bun test vence:
bun test começa a executar testes mais rápido do que o Vitest consegue terminar de carregar sua configuração.vitest.config.ts necessário para setups básicos.Onde Vitest ainda vence:
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.
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.
# 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 externalconst 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`);
}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ídaPara 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.
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.
Esta é a pergunta que eu mais recebo, então deixe-me ser bem específico.
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.
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:
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.
A imagem Docker oficial do Bun é bem mantida e pronta para produção.
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"]# 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"]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.
# 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.
Se você está considerando migrar para o Bun, aqui está o caminho incremental que eu recomendo:
# 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.
# 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.
# 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.
// 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.
# 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.
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.
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.
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.
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 limiteApós um ano usando o Bun, aqui estão as coisas que me pegaram:
// 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
});// 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)// 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/bun# 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âneobunfig.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"Após um ano de uso em produção, aqui é onde eu me posicionei:
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.
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 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.
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.