Bun in produzione: cosa funziona, cosa no, e cosa mi ha sorpreso
Bun come runtime, package manager, bundler e test runner. Benchmark reali, lacune di compatibilità con Node.js, pattern di migrazione e dove uso Bun in produzione oggi.
Ogni pochi anni, l'ecosistema JavaScript partorisce un nuovo runtime e il dibattito segue un arco prevedibile. Hype. Benchmark. "X è morto." Dose di realtà. Assestamento sui casi d'uso effettivi dove il nuovo strumento brilla davvero.
Bun si trova nel mezzo di quell'arco proprio adesso. E a differenza della maggior parte degli sfidanti, sta resistendo. Non perché sia "più veloce" (anche se spesso lo è), ma perché sta risolvendo un problema genuinamente diverso: la toolchain JavaScript ha troppe parti mobili, e Bun le comprime in una sola.
Uso Bun in varie forme da oltre un anno. In parte in produzione. In parte sostituendo strumenti che pensavo non avrei mai sostituito. Questo articolo è un resoconto onesto di cosa funziona, cosa no, e dove le lacune contano ancora.
Cos'è davvero Bun#
Il primo equivoco da chiarire: Bun non è "un Node.js più veloce." Questa definizione lo sminuisce.
Bun è quattro strumenti in un unico binario:
- Un runtime JavaScript/TypeScript -- esegue il tuo codice, come Node.js o Deno
- Un package manager -- sostituisce npm, yarn o pnpm
- Un bundler -- sostituisce esbuild, webpack o Rollup per certi casi d'uso
- Un test runner -- sostituisce Jest o Vitest per la maggior parte delle test suite
La differenza architetturale chiave rispetto a Node.js è il motore. Node.js usa V8 (il motore di Chrome). Bun usa JavaScriptCore (il motore di Safari). Entrambi sono motori maturi e production-grade, ma fanno tradeoff diversi. JavaScriptCore tende ad avere tempi di avvio più rapidi e minor overhead di memoria. V8 tende ad avere un throughput di picco migliore per computazioni a lungo termine. In pratica, queste differenze sono più piccole di quanto pensi per la maggior parte dei carichi di lavoro.
L'altro grande elemento differenziante: Bun è scritto in Zig, un linguaggio di programmazione di sistema che si posiziona grosso modo allo stesso livello del C ma con migliori garanzie di sicurezza della memoria. È per questo che Bun può essere così aggressivo con le performance -- Zig ti dà il tipo di controllo a basso livello che il C fornisce senza la densità di insidie del C.
# Check your Bun version
bun --version
# Run a TypeScript file directly — no tsconfig, no compilation step
bun run server.ts
# Install packages
bun install
# Run tests
bun test
# Bundle for production
bun build ./src/index.ts --outdir ./distQuesto è un singolo binario che fa il lavoro di node + npm + esbuild + vitest. Che ti piaccia o no, è una riduzione di complessità convincente.
Le affermazioni sulla velocità -- benchmark onesti#
Sarò diretto su questo: i benchmark di marketing di Bun sono selezionati ad hoc. Non fraudolenti -- selezionati. Mostrano gli scenari dove Bun performa meglio, che è esattamente quello che ti aspetti dal materiale di marketing. Il problema è che le persone estrapolano da quei benchmark per affermare che Bun è "25x più veloce" in tutto, il che non è assolutamente vero.
Ecco dove Bun è genuinamente, significativamente più veloce:
Tempo di avvio#
Questo è il più grande vantaggio genuino di Bun, e non è nemmeno lontanamente una competizione.
# Measuring startup time — run each 100 times
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# Typical results:
# node: ~40ms
# bun: ~6msÈ grossomodo una differenza di 6-7x nel tempo di avvio. Per script, tool da riga di comando e funzioni serverless dove il cold start conta, è significativo. Per un processo server a lunga esecuzione che parte una volta e gira per settimane, è irrilevante.
Installazione pacchetti#
Questa è l'altra area dove Bun mette in imbarazzo la concorrenza.
# Clean install benchmark — delete node_modules and lockfile first
rm -rf node_modules bun.lockb package-lock.json
# Time npm
time npm install
# Real: ~18.4s (typical medium-sized project)
# Time bun
time bun install
# Real: ~2.1sÈ una differenza di 8-9x, ed è costante. Le ragioni sono principalmente:
- Lockfile binario --
bun.lockbè un formato binario, non JSON. Più veloce da leggere e scrivere. - Cache globale -- Bun mantiene una cache globale dei moduli così le reinstallazioni tra progetti condividono i pacchetti scaricati.
- I/O di Zig -- Il package manager stesso è scritto in Zig, non JavaScript. Le operazioni di I/O su file sono più vicine al metallo.
- Strategia a symlink -- Bun usa hardlink dalla cache globale invece di copiare i file.
Throughput del server HTTP#
Il server HTTP integrato di Bun è veloce, ma i confronti necessitano contesto.
# Quick and dirty benchmark with bombardier
# Testing a simple "Hello World" response
# Bun server
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
# Node.js (native http module)
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: grossomodo 2x per risposte banali. Bun vs. Express: grossomodo 7x, ma è un confronto sleale perché Express aggiunge overhead di middleware. Nel momento in cui aggiungi logica reale -- query al database, autenticazione, serializzazione JSON di dati effettivi -- il divario si riduce drasticamente.
Dove la differenza è trascurabile#
Computazione CPU-bound:
// fibonacci.ts — this is engine-bound, not runtime-bound
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 # ~1580msNode.js (V8) in realtà vince leggermente qui. Il compilatore JIT di V8 è più aggressivo sui loop caldi. Per il lavoro CPU-bound, le differenze tra motori sono in pari -- a volte V8 vince, a volte JSC vince, e le differenze sono nel rumore.
Come fare i tuoi benchmark#
Non fidarti dei benchmark di nessuno, inclusi i miei. Ecco come misurare ciò che conta per il tuo specifico carico di lavoro:
# Install hyperfine for proper benchmarking
brew install hyperfine # macOS
# or: cargo install hyperfine
# Benchmark startup + execution of your actual app
hyperfine --warmup 3 \
'node dist/server.js' \
'bun src/server.ts' \
--prepare 'sleep 0.1'
# For HTTP servers, use bombardier or wrk
# Important: test with realistic payloads, not "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
# Memory comparison
/usr/bin/time -v node server.js # Linux
/usr/bin/time -l bun server.ts # macOSLa regola empirica: se il tuo collo di bottiglia è l'I/O (file system, rete, database), il vantaggio di Bun è modesto. Se il tuo collo di bottiglia è il tempo di avvio o la velocità della toolchain, Bun vince alla grande. Se il tuo collo di bottiglia è la computazione pura, è un testa a testa.
Bun come package manager#
Questo è dove ho fatto il passaggio completo. Anche nei progetti dove uso Node.js in produzione, uso bun install per lo sviluppo locale e il CI. È semplicemente più veloce, e la compatibilità è eccellente.
Le basi#
# Install all dependencies from package.json
bun install
# Add a dependency
bun add express
# Add a dev dependency
bun add -d vitest
# Remove a dependency
bun remove express
# Update a dependency
bun update express
# Install a specific version
bun add express@4.18.2Se hai usato npm o yarn, ti è completamente familiare. I flag sono leggermente diversi (-d invece di --save-dev), ma il modello mentale è identico.
La questione del lockfile#
Bun usa bun.lockb, un lockfile binario. Questo è sia il suo superpotere sia il suo più grande punto di frizione.
Il lato positivo: È drasticamente più veloce da leggere e scrivere. Il formato binario significa che Bun può analizzare il lockfile in microsecondi, non le centinaia di millisecondi che npm spende per il parsing di package-lock.json.
Il lato negativo: Non puoi esaminarlo in un diff. Se sei in un team e qualcuno aggiorna una dipendenza, non puoi guardare il diff del lockfile in una PR e vedere cos'è cambiato. Questo conta più di quanto i sostenitori della velocità vogliano ammettere.
# You can dump the lockfile to human-readable format
bun bun.lockb > lockfile-dump.txt
# Or use the built-in text output
bun install --yarn
# This generates a yarn.lock alongside bun.lockbIl mio approccio: committo bun.lockb nel repo e genero anche un yarn.lock o package-lock.json come fallback leggibile. Cintura e bretelle.
Supporto workspace#
Bun supporta i workspace in stile npm/yarn:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# Install dependencies for all workspaces
bun install
# Run a script in a specific workspace
bun run --filter packages/shared build
# Add a dependency to a specific workspace
bun add react --filter apps/webIl supporto workspace è solido ed è migliorato significativamente. La lacuna principale rispetto a pnpm è che la risoluzione delle dipendenze dei workspace di Bun è meno rigorosa -- la severità di pnpm è una feature per i monorepo perché intercetta le dipendenze fantasma.
Compatibilità con progetti esistenti#
Puoi inserire bun install in quasi qualsiasi progetto Node.js esistente. Legge package.json, rispetta .npmrc per la configurazione del registry e gestisce correttamente le peerDependencies. La transizione è tipicamente:
# Step 1: Delete existing lockfile and node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
# Step 2: Install with Bun
bun install
# Step 3: Verify your app still works
bun run dev
# or: node dist/server.js (Bun package manager, Node runtime)L'ho fatto su una dozzina di progetti e ho avuto zero problemi con il package manager in sé. L'unica insidia è se la tua pipeline CI cerca specificamente package-lock.json -- dovrai aggiornarla per gestire bun.lockb.
Compatibilità con Node.js#
Questa è la sezione dove devo essere più attento, perché la situazione cambia ogni mese. A inizio 2026, ecco il quadro onesto.
Cosa funziona#
La stragrande maggioranza dei pacchetti npm funziona senza modifiche. Bun implementa la maggior parte dei moduli built-in di Node.js:
// These all work as expected in 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";Sia CommonJS che ESM funzionano. require() e import possono coesistere. TypeScript gira senza alcuno step di compilazione -- Bun rimuove i tipi in fase di parsing.
Framework che funzionano:
- Express -- funziona, incluso l'ecosistema di middleware
- Fastify -- funziona
- Hono -- funziona (ed è eccellente con Bun)
- Next.js -- funziona con dei caveat (più avanti su questo)
- Prisma -- funziona
- Drizzle ORM -- funziona
- Socket.io -- funziona
Cosa non funziona (o ha problemi)#
Le lacune tendono a ricadere in poche categorie:
Addon nativi (node-gyp): Se un pacchetto usa addon C++ compilati con node-gyp, potrebbe non funzionare con Bun. Bun ha il suo sistema FFI e supporta molti moduli nativi, ma la copertura non è al 100%. Ad esempio, bcrypt (quello nativo) ha avuto problemi -- usa bcryptjs al suo posto.
# Check if a package uses native addons
ls node_modules/your-package/binding.gyp # If this exists, it's nativeInterni specifici di Node.js: Alcuni pacchetti accedono agli interni di Node.js come process.binding() o usano API specifiche di V8. Questi non funzioneranno in Bun dato che gira su JavaScriptCore.
// This will NOT work in Bun — V8-specific
const v8 = require("v8");
v8.serialize({ data: "test" });
// This WILL work — use Bun's equivalent or a cross-runtime approach
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));Worker thread: Bun supporta Web Workers e node:worker_threads, ma ci sono casi limite. Alcuni pattern di utilizzo avanzati -- specialmente attorno a SharedArrayBuffer e Atomics -- possono comportarsi diversamente.
Modulo vm: node:vm ha supporto parziale. Se il tuo codice o una dipendenza usa estensivamente vm.createContext() (alcuni motori di template lo fanno), testa a fondo.
Il tracker di compatibilità#
Bun mantiene un tracker di compatibilità ufficiale. Controllalo prima di impegnarti con Bun per un progetto:
# Run Bun's built-in compatibility check on your project
bun --bun node_modules/.bin/your-tool
# The --bun flag forces Bun's runtime even for node_modules scriptsIl mio consiglio: non dare per scontata la compatibilità. Lancia la tua test suite sotto Bun prima di decidere. Ci vogliono cinque minuti e ti risparmia ore di debug.
# Quick compatibility check — run your full test suite under Bun
bun test # If you use bun test runner
# or
bun run vitest # If you use vitestLe API integrate di Bun#
Qui Bun diventa interessante. Invece di limitarsi a reimplementare le API di Node.js, Bun fornisce API proprie progettate per essere più semplici e veloci.
Bun.serve() -- il server HTTP integrato#
Questa è l'API che uso di più. È pulita, veloce, e il supporto WebSocket è integrato direttamente.
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}`);Alcune cose da notare:
- Request/Response standard web -- nessuna API proprietaria. L'handler
fetchriceve unaRequeststandard e restituisce unaResponsestandard. Se hai scritto un Cloudflare Worker, ti sembrerà identico. Response.json()-- helper integrato per risposte JSON.- Nessun import necessario --
Bun.serveè un globale. Nienterequire("http").
Ecco un esempio più realistico con routing, parsing del body JSON e gestione degli errori:
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}`);Questa è un'API CRUD completa con SQLite in circa 50 righe. Niente Express, niente ORM, niente catena di middleware. Per piccole API e tool interni, questo è il mio setup di riferimento ormai.
Bun.file() e Bun.write() -- I/O su file#
L'API file di Bun è piacevolmente semplice rispetto a fs.readFile():
// Reading files
const file = Bun.file("./config.json");
const text = await file.text(); // Read as string
const json = await file.json(); // Parse as JSON directly
const bytes = await file.arrayBuffer(); // Read as ArrayBuffer
const stream = file.stream(); // Read as ReadableStream
// File metadata
console.log(file.size); // Size in bytes
console.log(file.type); // MIME type (e.g., "application/json")
// Writing files
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"));
// Write a Response body to a file
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);L'API Bun.file() è lazy -- non legge il file finché non chiami .text(), .json(), ecc. Questo significa che puoi passare riferimenti Bun.file() senza incorrere in costi di I/O finché non hai effettivamente bisogno dei dati.
Supporto WebSocket integrato#
I WebSocket sono first-class in 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 to all subscribers
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`Client disconnected: ${ws.data.userId}`);
ws.unsubscribe("chat");
},
},
});Il pattern server.publish() e ws.subscribe() è un pub/sub integrato. Niente Redis, nessuna libreria WebSocket separata. Per funzionalità real-time semplici, è incredibilmente comodo.
SQLite integrato con bun:sqlite#
Questo è quello che mi ha sorpreso di più. Bun include SQLite integrato direttamente nel runtime:
import { Database } from "bun:sqlite";
// Open or create a database
const db = new Database("myapp.db");
// WAL mode for better concurrent read performance
db.exec("PRAGMA journal_mode = WAL");
// Create tables
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 (reusable, faster for repeated queries)
const insertUser = db.prepare(
"INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = $email"
);
// Usage
const user = insertUser.get({
$email: "alice@example.com",
$name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
// Transactions
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`);Questo è SQLite sincrono con le performance di una libreria C (perché lo è -- Bun incorpora libsqlite3 direttamente). Per tool da riga di comando, app local-first e piccoli servizi, SQLite integrato significa zero dipendenze esterne per il tuo layer dati.
Bun test runner#
bun test è un sostituto drop-in per Jest nella maggior parte dei casi. Usa la stessa API describe/it/expect e supporta la maggior parte dei matcher di Jest.
Utilizzo base#
// 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);
});
});# Run all tests
bun test
# Run specific file
bun test math.test.ts
# Run tests matching a pattern
bun test --test-name-pattern "adds numbers"
# Watch mode
bun test --watch
# Coverage
bun test --coverageMocking#
Bun supporta mocking compatibile con Jest:
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
// Mock a module
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");
});
});
// Spy on an object method
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 -- il mio confronto onesto#
Uso Vitest per questo progetto (e per la maggior parte dei miei progetti). Ecco perché non ho ancora fatto il passaggio completo:
Dove vince bun test:
- Velocità di avvio.
bun testinizia a eseguire i test prima che Vitest finisca di caricare la sua configurazione. - Zero configurazione. Nessun
vitest.config.tsnecessario per setup di base. - TypeScript integrato. Nessuno step di trasformazione.
Dove vince ancora Vitest:
- Ecosistema: Vitest ha più plugin, migliore integrazione IDE e una community più ampia.
- Configurazione: Il sistema di configurazione di Vitest è più flessibile. Reporter personalizzati, file di setup complessi, ambienti di test multipli.
- Modalità browser: Vitest può eseguire test in un browser reale. Bun no.
- Compatibilità: Alcune librerie di testing (Testing Library, MSW) sono state testate più a fondo con Vitest/Jest.
- Snapshot testing: Entrambi lo supportano, ma l'implementazione di Vitest è più matura con un output diff migliore.
Per un nuovo progetto con esigenze di testing semplici, userei bun test. Per un progetto consolidato con Testing Library, MSW e mocking complesso, tengo Vitest.
Bun bundler#
bun build è un bundler JavaScript/TypeScript veloce. Non è un sostituto di webpack -- è più nella categoria di esbuild: veloce, opinato e focalizzato sui casi comuni.
Bundling di base#
# Bundle a single entry point
bun build ./src/index.ts --outdir ./dist
# Bundle for different targets
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
# Minify
bun build ./src/index.ts --outdir ./dist --minify
# Generate sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap externalAPI programmatica#
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"], // Don't bundle these
naming: "[dir]/[name]-[hash].[ext]",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.path} — ${output.size} bytes`);
}Tree-shaking#
Bun supporta il tree-shaking per 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
# The `unused` function won't appear in the outputDove Bun build è carente#
- Nessun bundling CSS -- Ti serve uno strumento separato per il CSS (PostCSS, Lightning CSS, Tailwind CLI).
- Nessuna generazione HTML -- Fa il bundle di JavaScript/TypeScript, non di web app complete.
- Ecosistema plugin -- esbuild ha un ecosistema di plugin molto più ampio. L'API plugin di Bun è compatibile ma la community è più piccola.
- Code splitting avanzato -- Webpack e Rollup offrono ancora strategie di chunk più sofisticate.
Per buildare una libreria o il bundle JS di una semplice web app, bun build è eccellente. Per build complesse di app con CSS modules, ottimizzazione immagini e strategie di chunk personalizzate, ti servirà ancora un bundler completo.
Macro di Bun#
Una feature genuinamente unica: esecuzione di codice a tempo di compilazione tramite macro.
// build-info.ts — this file runs at BUILD TIME, not 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() executes at bundle time
// The result is inlined as a static value
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);Dopo il bundling, getBuildInfo() viene sostituito con l'oggetto letterale -- nessuna chiamata di funzione a runtime, nessun import di child_process. Il codice è stato eseguito durante la build e il risultato è stato inlinato. Questo è potente per incorporare metadati di build, feature flag o configurazione specifica per ambiente.
Usare Bun con Next.js#
Questa è la domanda che mi fanno più spesso, quindi sarò molto specifico.
Cosa funziona oggi#
Bun come package manager per Next.js -- funziona perfettamente:
# Use Bun to install dependencies, then use Node.js to run Next.js
bun install
bun run dev # This actually runs the "dev" script via Node.js by default
bun run build
bun run startQuesto è quello che faccio per ogni progetto Next.js. Il comando bun run <script> legge la sezione scripts di package.json e la esegue. Di default, usa il Node.js del sistema per l'esecuzione effettiva. Ottieni l'installazione veloce dei pacchetti di Bun senza cambiare runtime.
Runtime Bun per lo sviluppo Next.js:
# Force Next.js to run under Bun's runtime
bun --bun run devQuesto funziona per lo sviluppo nella maggior parte dei casi. Il flag --bun dice a Bun di usare il proprio runtime invece di delegare a Node.js. L'hot module replacement funziona. Le API route funzionano. I server component funzionano.
Cosa è ancora sperimentale#
Runtime Bun per build di produzione Next.js:
# Build with Bun runtime
bun --bun run build
# Start production server with Bun runtime
bun --bun run startQuesto funziona per molti progetti ma ho incontrato casi limite:
- Alcuni comportamenti del middleware differiscono -- Se usi middleware Next.js che dipende da API specifiche di Node.js, potresti avere problemi di compatibilità.
- Ottimizzazione immagini -- La pipeline di ottimizzazione immagini di Next.js usa sharp, che è un addon nativo. Funziona con Bun, ma ho visto problemi occasionali.
- ISR (Incremental Static Regeneration) -- Funziona, ma non l'ho stress-testato sotto Bun in produzione.
La mia raccomandazione per Next.js#
Usa Bun come package manager. Usa Node.js come runtime. Questo ti dà i benefici di velocità di bun install senza alcun rischio di compatibilità.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# Daily workflow
bun install # Fast package installation
bun run dev # Runs "next dev" via Node.js
bun run build # Runs "next build" via Node.jsQuando la compatibilità di Bun con Node.js raggiungerà il 100% per l'utilizzo interno di Next.js (ci siamo quasi, ma non ancora), farò il passaggio. Fino ad allora, il solo package manager mi fa risparmiare abbastanza tempo da giustificare l'installazione.
Docker con Bun#
L'immagine Docker ufficiale di Bun è ben mantenuta e pronta per la produzione.
Dockerfile base#
FROM oven/bun:1 AS base
WORKDIR /app
# Install dependencies
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Build (if needed)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Production
FROM base AS production
WORKDIR /app
# Don't run as root
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]Build multi-stage per immagine minimale#
# Build stage: full Bun image with all dependencies
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
# Runtime stage: smaller base image
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"]Compilazione in un binario singolo#
Questa è una delle killer feature di Bun per il deploy:
# Compile your app into a single executable
bun build --compile ./src/server.ts --outfile server
# The output is a standalone binary — no Bun or Node.js needed to run it
./server# Ultra-minimal Docker image using compiled binary
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
# Final image — just the binary
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"]Il binario compilato è tipicamente 50-90 MB (include il runtime Bun). È più grande di un binario Go ma molto più piccolo di un'installazione completa di Node.js più node_modules. Per i deploy containerizzati, la natura self-contained è una semplificazione significativa.
Confronto dimensioni#
# Node.js image
docker images | grep node
# node:20-slim ~180MB
# Bun image
docker images | grep bun
# oven/bun:1-slim ~130MB
# Compiled binary on debian:bookworm-slim
# ~80MB base + ~70MB binary = ~150MB total
# vs. Alpine with Node.js
# node:20-alpine ~130MB + node_modulesL'approccio con binario elimina node_modules completamente dall'immagine finale. Nessun npm install in produzione. Nessuna superficie di attacco dalla supply chain di centinaia di pacchetti. Solo un file.
Pattern di migrazione#
Se stai valutando il passaggio a Bun, ecco il percorso incrementale che raccomando:
Fase 1: solo package manager (rischio zero)#
# Replace npm/yarn/pnpm with bun install
# Change your CI pipeline:
# Before:
npm ci
# After:
bun install --frozen-lockfileNessuna modifica al codice. Nessuna modifica al runtime. Solo installazioni più veloci. Se qualcosa si rompe (non succederà), ripristina cancellando bun.lockb e lanciando npm install.
Fase 2: script e tooling#
# Use bun to run development scripts
bun run dev
bun run lint
bun run format
# Use bun for one-off scripts
bun run scripts/seed-database.ts
bun run scripts/migrate.tsContinui a usare Node.js come runtime per la tua applicazione vera e propria. Ma gli script beneficiano dell'avvio più rapido di Bun e del supporto nativo a TypeScript.
Fase 3: test runner (rischio medio)#
# Replace vitest/jest with bun test for simple test suites
bun test
# Keep vitest for complex test setups
# (Testing Library, MSW, custom environments)Lancia la tua intera test suite sotto bun test. Se tutto passa, hai eliminato una devDependency. Se alcuni test falliscono per problemi di compatibilità, tieni Vitest per quelli e usa bun test per il resto.
Fase 4: runtime per nuovi servizi (rischio calcolato)#
// New microservices or APIs — start with Bun from day one
Bun.serve({
port: 3000,
fetch(req) {
// Your new service here
},
});Non migrare servizi Node.js esistenti al runtime Bun. Scrivi invece nuovi servizi con Bun fin dall'inizio. Questo limita il tuo raggio d'esplosione.
Fase 5: migrazione del runtime (avanzata)#
# Only after thorough testing:
# Replace node with bun for existing services
# Before:
node dist/server.js
# After:
bun dist/server.jsRaccomando questo solo per servizi con un'eccellente copertura di test. Lancia i tuoi test di carico sotto Bun prima di switchare la produzione.
Variabili d'ambiente e configurazione#
Bun gestisce i file .env automaticamente -- nessun pacchetto dotenv necessario:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// These are available without any import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun-specific alternativeBun carica .env, .env.local, .env.production, ecc. automaticamente, seguendo la stessa convenzione di Next.js. Una dipendenza in meno nel tuo package.json.
Gestione errori e debug#
L'output degli errori di Bun è migliorato significativamente, ma in alcuni casi non è ancora rifinito come quello di Node.js:
# Bun's debugger — works with VS Code
bun --inspect run server.ts
# Bun's inspect-brk — pause on first line
bun --inspect-brk run server.tsPer VS Code, aggiungi questo al tuo .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
}
]
}Gli stack trace in Bun sono generalmente accurati e includono le source map per TypeScript. La lacuna principale nel debug è che alcuni strumenti specifici di Node.js (come ndb o clinic.js) non funzionano con Bun.
Considerazioni sulla sicurezza#
Alcune cose a cui pensare se stai valutando Bun per la produzione:
Maturità: Node.js è in produzione da oltre 15 anni. Ogni caso limite nel parsing HTTP, nella gestione TLS e nell'elaborazione degli stream è stato trovato e corretto. Bun è più giovane. È ben testato, ma la superficie per bug non ancora scoperti è più ampia.
Patch di sicurezza: Il team Bun rilascia aggiornamenti frequentemente, ma il team di sicurezza di Node.js ha un processo CVE formale, disclosure coordinata e un track record più lungo. Per applicazioni critiche in termini di sicurezza, questo conta.
Supply chain: Le funzionalità integrate di Bun (SQLite, server HTTP, WebSocket) significano meno dipendenze npm. Meno dipendenze significa una superficie di attacco più piccola per la supply chain. Questo è un vantaggio di sicurezza genuino.
# Compare dependency counts
# A typical Express + SQLite + WebSocket project:
npm ls --all | wc -l
# ~340 packages
# The same functionality with Bun built-ins:
bun pm ls --all | wc -l
# ~12 packages (just your application code)È una riduzione significativa nel numero di pacchetti a cui affidi il tuo carico di lavoro in produzione.
Ottimizzazione delle performance#
Alcuni consigli di performance specifici per Bun:
// Use Bun.serve() options for production tuning
Bun.serve({
port: 3000,
// Increase max request body size (default is 128MB)
maxRequestBodySize: 1024 * 1024 * 50, // 50MB
// Enable development mode for better error pages
development: process.env.NODE_ENV !== "production",
// Reuse port (useful for zero-downtime restarts)
reusePort: true,
fetch(req) {
return new Response("OK");
},
});// Use Bun.Transpiler for runtime code transformation
const transpiler = new Bun.Transpiler({
loader: "tsx",
target: "browser",
});
const code = transpiler.transformSync(`
const App: React.FC = () => <div>Hello</div>;
export default App;
`);# Bun's memory usage flags
bun --smol run server.ts # Reduce memory footprint (slightly slower)
# Set max heap size
BUN_JSC_forceRAMSize=512000000 bun run server.ts # ~512MB limitTrabocchetti comuni#
Dopo un anno di utilizzo di Bun, ecco le cose che mi hanno fatto inciampare:
1. Il comportamento del fetch globale differisce#
// Node.js 18+ fetch and Bun's fetch are slightly different
// in how they handle certain headers and redirects
// Bun follows redirects by default (like browsers)
// Node.js fetch also follows redirects, but the behavior
// with certain status codes (303, 307, 308) can differ
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // Be explicit about redirect handling
});2. Comportamento all'uscita del processo#
// Bun exits when the event loop is empty
// Node.js sometimes keeps running due to lingering handles
// If your Bun script exits unexpectedly, something isn't
// keeping the event loop alive
// This will exit immediately in Bun:
setTimeout(() => {}, 0);
// This will keep running:
setTimeout(() => {}, 1000);
// (Bun exits after the timeout fires)3. Configurazione TypeScript#
// Bun has its own tsconfig defaults
// If you're sharing a project between Bun and Node.js,
// be explicit in your tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Add Bun type definitions
}
}# Install Bun types
bun add -d @types/bun4. Hot reload in sviluppo#
# Bun has built-in watch mode
bun --watch run server.ts
# This restarts the process on file changes
# It's not HMR (Hot Module Replacement) — it's a full restart
# But because Bun starts so fast, it feels instant5. Il file di configurazione bunfig.toml#
# bunfig.toml — Bun's config file (optional)
[install]
# Use a private registry
registry = "https://npm.mycompany.com"
# Scoped registries
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
[test]
# Test configuration
coverage = true
coverageReporter = ["text", "lcov"]
[run]
# Shell to use for bun run
shell = "bash"Il mio verdetto#
Dopo un anno di utilizzo in produzione, ecco dove mi sono assestato:
Dove uso Bun oggi#
Package manager per tutti i progetti -- incluso questo blog Next.js. bun install è più veloce, e la compatibilità è essenzialmente perfetta. Non vedo motivo di usare npm o yarn. pnpm è l'unica alternativa che prenderei in considerazione (per la sua risoluzione rigorosa delle dipendenze nei monorepo).
Runtime per script e tool CLI -- Qualsiasi file TypeScript che devo eseguire una tantum, lo lancio con bun. Nessuno step di compilazione. Avvio veloce. Caricamento .env integrato. Ha sostituito completamente ts-node e tsx nel mio workflow.
Runtime per piccole API e tool interni -- Bun.serve() + bun:sqlite è uno stack incredibilmente produttivo per tool interni, handler di webhook e piccoli servizi. Il modello di deploy "un binario, zero dipendenze" è convincente.
Test runner per progetti semplici -- Per progetti con esigenze di testing semplici, bun test è veloce e non richiede configurazione.
Dove resto con Node.js#
Next.js in produzione -- Non perché Bun non funzioni, ma perché il rapporto rischio-beneficio non lo giustifica ancora. Next.js è un framework complesso con molti punti di integrazione. Voglio il runtime più battletestato al di sotto.
Servizi critici in produzione -- I miei server API principali girano su Node.js dietro PM2. L'ecosistema di monitoraggio, gli strumenti di debug, la conoscenza operativa -- è tutto Node.js. Bun ci arriverà, ma non ci è ancora.
Qualsiasi cosa con addon nativi -- Se una catena di dipendenze include addon nativi C++, non provo nemmeno Bun. Non vale la pena debuggare i problemi di compatibilità.
Team che non conoscono Bun -- Introdurre Bun come runtime a un team che non lo ha mai usato aggiunge carico cognitivo. Come package manager, va bene. Come runtime, aspetta che il team sia pronto.
Cosa tengo d'occhio#
Il tracker di compatibilità di Bun -- Quando raggiungerà il 100% per le API Node.js che mi interessano, rivaluterò.
Supporto framework -- Next.js, Remix e SvelteKit hanno tutti livelli variabili di supporto Bun. Quando uno di loro supporterà ufficialmente Bun come runtime di produzione, quello sarà un segnale.
Adozione enterprise -- Quando aziende con SLA reali useranno Bun in produzione e ne scriveranno, la questione della maturità sarà risolta.
La linea di release 1.2+ -- Bun si muove velocemente. Le feature arrivano ogni settimana. Il Bun che uso oggi è significativamente migliore del Bun che ho provato un anno fa.
Conclusioni#
Bun non è una soluzione magica. Non renderà veloce un'app lenta e non renderà ben progettata un'API mal progettata. Ma è un miglioramento genuino nell'esperienza di sviluppo per l'ecosistema JavaScript.
La cosa che apprezzo di più di Bun non è una singola feature. È la riduzione della complessità della toolchain. Un unico binario che installa pacchetti, esegue TypeScript, fa il bundle del codice e lancia i test. Nessun tsconfig.json per gli script. Nessun Babel. Nessuna configurazione separata per il test runner. Solo bun run il-tuo-file.ts e funziona.
Il consiglio pratico: parti con bun install. Rischio zero, beneficio immediato. Poi prova bun run per gli script. Poi valuta il resto in base alle tue esigenze specifiche. Non devi andare all-in. Bun funziona perfettamente come sostituto parziale, e probabilmente è così che la maggior parte delle persone dovrebbe usarlo oggi.
Il panorama dei runtime JavaScript è migliore con Bun al suo interno. La competizione sta rendendo migliore anche Node.js -- Node.js 22+ è diventato significativamente più veloce, in parte in risposta alla pressione di Bun. Vincono tutti.