Bun na produkcji: Co działa, co nie, i co mnie zaskoczyło
Bun jako runtime, menedżer pakietów, bundler i test runner. Realne benchmarki, luki kompatybilności z Node.js, wzorce migracji i gdzie używam Bun na produkcji dzisiaj.
Co kilka lat ekosystem JavaScript dostaje nowy runtime i dyskurs podąża przewidywalnym łukiem. Hype. Benchmarki. „X nie żyje." Weryfikacja rzeczywistością. Osiadanie w faktycznych przypadkach użycia, gdzie nowe narzędzie naprawdę świeci.
Bun jest teraz w środku tego łuku. I w przeciwieństwie do większości pretendentów, trzyma się. Nie dlatego, że jest „szybszy" (choć często jest), ale dlatego, że rozwiązuje naprawdę inny problem: toolchain JavaScript ma za dużo ruchomych części, a Bun sprowadza je do jednej.
Używam Bun w różnych konfiguracjach od ponad roku. Część na produkcji. Część zastępując narzędzia, które myślałem, że nigdy nie zastąpię. Ten post to uczciwe rozliczenie z tego, co działa, co nie, i gdzie luki wciąż mają znaczenie.
Czym tak naprawdę jest Bun#
Pierwsze nieporozumienie do wyjaśnienia: Bun to nie „szybszy Node.js." Takie ujęcie nie oddaje mu sprawiedliwości.
Bun to cztery narzędzia w jednym pliku binarnym:
- Runtime JavaScript/TypeScript — uruchamia twój kod, jak Node.js czy Deno
- Menedżer pakietów — zastępuje npm, yarn lub pnpm
- Bundler — zastępuje esbuild, webpack lub Rollup w określonych przypadkach
- Test runner — zastępuje Jest lub Vitest dla większości zestawów testów
Kluczowa różnica architektoniczna względem Node.js to silnik. Node.js używa V8 (silnik Chrome). Bun używa JavaScriptCore (silnik Safari). Oba to dojrzałe, produkcyjne silniki, ale robią inne kompromisy. JavaScriptCore zwykle ma szybszy czas startu i mniejszy narzut pamięciowy. V8 zwykle ma lepszą szczytową przepustowość przy długo trwających obliczeniach. W praktyce te różnice są mniejsze niż myślisz dla większości obciążeń.
Drugi główny wyróżnik: Bun jest napisany w Zig, systemowym języku programowania, który działa mniej więcej na tym samym poziomie co C, ale z lepszymi gwarancjami bezpieczeństwa pamięci. Właśnie dlatego Bun może być tak agresywny z wydajnością — Zig daje ci niskopoziomową kontrolę, jaką zapewnia C, bez gęstości pułapek 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 ./distTo jeden plik binarny robiący robotę node + npm + esbuild + vitest. Lubisz to albo nie, to przekonująca redukcja złożoności.
Twierdzenia o szybkości — uczciwe benchmarki#
Powiem wprost: marketingowe benchmarki Bun są wybiorcze. Nie sfałszowane — wybiorcze. Pokazują scenariusze, w których Bun wypada najlepiej, co jest dokładnie tym, czego oczekujesz od materiałów marketingowych. Problem polega na tym, że ludzie ekstrapolują z tych benchmarków twierdząc, że Bun jest „25x szybszy" we wszystkim, co absolutnie nie jest prawdą.
Oto gdzie Bun jest naprawdę, znacząco szybszy:
Czas startu#
To jest największa prawdziwa przewaga Bun i to nawet nie jest blisko.
# 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: ~6msTo mniej więcej 6-7x różnica w czasie startu. Dla skryptów, narzędzi CLI i funkcji serverless, gdzie zimny start ma znaczenie, jest to istotne. Dla długo działającego procesu serwera, który startuje raz i działa tygodniami, to bez znaczenia.
Instalacja pakietów#
To jest drugi obszar, gdzie Bun zawstydza konkurencję.
# 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.1sTo 8-9x różnica i jest konsekwentna. Przyczyny to głównie:
- Binarny lockfile —
bun.lockbto format binarny, nie JSON. Szybszy do odczytu i zapisu. - Globalny cache — Bun utrzymuje globalny cache modułów, więc reinstalacje między projektami współdzielą pobrane pakiety.
- I/O Zig — Sam menedżer pakietów jest napisany w Zig, nie JavaScript. Operacje I/O na plikach są bliżej sprzętu.
- Strategia symlinkowania — Bun używa hardlinków z globalnego cache'u zamiast kopiowania plików.
Przepustowość serwera HTTP#
Wbudowany serwer HTTP Bun jest szybki, ale porównania wymagają kontekstu.
# 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. surowy Node.js: mniej więcej 2x dla trywialnych odpowiedzi. Bun vs. Express: mniej więcej 7x, ale to niesprawiedliwe porównanie, bo Express dodaje narzut middleware. W momencie, gdy dodasz prawdziwą logikę — zapytania bazodanowe, uwierzytelnianie, serializacja JSON rzeczywistych danych — różnica dramatycznie się kurczy.
Gdzie różnica jest znikoma#
Obliczenia 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) tutaj faktycznie minimalnie wygrywa. Kompilator JIT V8 jest bardziej agresywny na gorących pętlach. W obliczeniach CPU-bound różnice między silnikami się wyrównują — czasem wygrywa V8, czasem JSC, i różnice są w granicach szumu.
Jak przeprowadzić własne benchmarki#
Nie ufaj niczyim benchmarkom, w tym moim. Oto jak zmierzyć to, co ma znaczenie dla twojego konkretnego obciążenia:
# 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 # macOSZasada kciuka: jeśli twoim wąskim gardłem jest I/O (system plików, sieć, baza danych), przewaga Bun jest umiarkowana. Jeśli twoim wąskim gardłem jest czas startu lub szybkość toolchaina, Bun wygrywa dużo. Jeśli twoim wąskim gardłem są surowe obliczenia, wynik jest remisowy.
Bun jako menedżer pakietów#
Tutaj przeszedłem całkowicie. Nawet w projektach, gdzie uruchamiam Node.js na produkcji, używam bun install do lokalnego developmentu i CI. Jest po prostu szybszy, a kompatybilność jest doskonała.
Podstawy#
# 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.2Jeśli używałeś npm lub yarn, jest to całkowicie znajome. Flagi są nieco inne (-d zamiast --save-dev), ale model mentalny jest identyczny.
Sytuacja z lockfile#
Bun używa bun.lockb, binarnego lockfile'a. To jest jednocześnie jego supermocem i największym punktem tarcia.
Plusy: Jest dramatycznie szybszy do odczytu i zapisu. Format binarny oznacza, że Bun może sparsować lockfile w mikrosekundach, a nie w setkach milisekund, które npm spędza na parsowaniu package-lock.json.
Minusy: Nie możesz go przejrzeć w diffie. Jeśli jesteś w zespole i ktoś zaktualizuje zależność, nie możesz spojrzeć na diff lockfile'a w PR i zobaczyć co się zmieniło. Ma to większe znaczenie, niż zwolennicy szybkości chcą przyznać.
# 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.lockbMoje podejście: commituję bun.lockb do repo i dodatkowo generuję yarn.lock lub package-lock.json jako czytelny fallback. Pasek i szelki.
Wsparcie workspace'ów#
Bun wspiera workspace'y w stylu 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/webWsparcie workspace'ów jest solidne i znacząco się poprawiło. Główna luka w porównaniu z pnpm jest taka, że rozwiązywanie zależności workspace w Bun jest mniej rygorystyczne — rygorystyczność pnpm jest zaletą dla monorepo, bo łapie fantomowe zależności.
Kompatybilność z istniejącymi projektami#
Możesz wrzucić bun install do prawie każdego istniejącego projektu Node.js. Czyta package.json, respektuje .npmrc do konfiguracji rejestru i obsługuje peerDependencies poprawnie. Przejście wygląda typowo tak:
# 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)Zrobiłem to na kilkunastu projektach i miałem zero problemów z samym menedżerem pakietów. Jedyny haczyk jest wtedy, gdy twój pipeline CI specjalnie szuka package-lock.json — musisz go zaktualizować, żeby obsługiwał bun.lockb.
Kompatybilność z Node.js#
To jest sekcja, w której muszę być najbardziej ostrożny, bo sytuacja zmienia się co miesiąc. Stan na początek 2026 — oto uczciwy obraz.
Co działa#
Zdecydowana większość pakietów npm działa bez modyfikacji. Bun implementuje większość wbudowanych modułów 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";Zarówno CommonJS jak i ESM działają. require() i import mogą współistnieć. TypeScript działa bez żadnego kroku kompilacji — Bun usuwa typy w trakcie parsowania.
Frameworki, które działają:
- Express — działa, włącznie z ekosystemem middleware
- Fastify — działa
- Hono — działa (i jest doskonały z Bun)
- Next.js — działa z zastrzeżeniami (więcej o tym niżej)
- Prisma — działa
- Drizzle ORM — działa
- Socket.io — działa
Co nie działa (lub ma problemy)#
Luki zwykle wpadają w kilka kategorii:
Natywne addony (node-gyp): Jeśli pakiet używa addonów C++ skompilowanych z node-gyp, może nie działać z Bun. Bun ma własny system FFI i wspiera wiele natywnych modułów, ale pokrycie nie jest 100%. Na przykład bcrypt (natywny) miał problemy — zamiast tego użyj bcryptjs.
# Check if a package uses native addons
ls node_modules/your-package/binding.gyp # If this exists, it's nativeSpecyficzne wewnętrzne mechanizmy Node.js: Niektóre pakiety sięgają do wewnętrznych mechanizmów Node.js jak process.binding() lub używają API specyficznych dla V8. To nie zadziała w Bun, bo działa na 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 threads: Bun wspiera Web Workers i node:worker_threads, ale istnieją przypadki brzegowe. Niektóre zaawansowane wzorce użycia — szczególnie wokół SharedArrayBuffer i Atomics — mogą się zachowywać inaczej.
Moduł vm: node:vm ma częściowe wsparcie. Jeśli twój kod lub zależność intensywnie używa vm.createContext() (niektóre silniki szablonów to robią), przetestuj dokładnie.
Tracker kompatybilności#
Bun utrzymuje oficjalny tracker kompatybilności. Sprawdź go zanim zdecydujesz się na Bun w projekcie:
# 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 scriptsMoja rekomendacja: nie zakładaj kompatybilności. Uruchom swój zestaw testów pod Bun zanim podejmiesz decyzję. To zajmuje pięć minut i oszczędza godziny debugowania.
# 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 vitestWbudowane API Bun#
Tutaj Bun robi się interesujący. Zamiast po prostu reimplementować API Node.js, Bun dostarcza własne API zaprojektowane tak, by były prostsze i szybsze.
Bun.serve() — wbudowany serwer HTTP#
To API, którego używam najczęściej. Jest czyste, szybkie, a wsparcie WebSocket jest wbudowane.
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}`);Kilka rzeczy do zauważenia:
- Standardowe Request/Response z Web API — żadne własnościowe API. Handler
fetchotrzymuje standardowyRequesti zwraca standardowyResponse. Jeśli pisałeś Cloudflare Worker, to wygląda identycznie. Response.json()— wbudowany helper do odpowiedzi JSON.- Nie trzeba importować —
Bun.servejest globalny. Żadnegorequire("http").
Oto bardziej realistyczny przykład z routingiem, parsowaniem JSON body i obsługą błędów:
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}`);To pełne CRUD API z SQLite w około 50 liniach. Bez Express, bez ORM, bez łańcucha middleware. Dla małych API i narzędzi wewnętrznych to teraz mój domyślny setup.
Bun.file() i Bun.write() — operacje na plikach#
API plikowe Bun jest odświeżająco proste w porównaniu z 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);API Bun.file() jest leniwe — nie czyta pliku dopóki nie wywołasz .text(), .json(), itd. Oznacza to, że możesz przekazywać referencje Bun.file() bez ponoszenia kosztów I/O, dopóki faktycznie nie potrzebujesz danych.
Wbudowane wsparcie WebSocket#
WebSockety są pierwszorzędnym obywatelem w 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");
},
},
});Wzorzec server.publish() i ws.subscribe() to wbudowany pub/sub. Bez Redis, bez osobnej biblioteki WebSocket. Dla prostych funkcji czasu rzeczywistego to niezwykle wygodne.
Wbudowany SQLite z bun:sqlite#
To mnie zaskoczyło najbardziej. Bun ma SQLite wbudowany bezpośrednio w 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`);To synchroniczny SQLite z wydajnością biblioteki C (bo to właśnie jest — Bun osadza libsqlite3 bezpośrednio). Dla narzędzi CLI, aplikacji local-first i małych serwisów wbudowany SQLite oznacza zero zewnętrznych zależności dla twojej warstwy danych.
Test runner Bun#
bun test to drop-in replacement dla Jest w większości przypadków. Używa tego samego API describe/it/expect i wspiera większość matcherów Jest.
Podstawowe użycie#
// 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 --coverageMockowanie#
Bun wspiera mockowanie kompatybilne z 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 — moje uczciwe porównanie#
Używam Vitest w tym projekcie (i większości moich projektów). Oto dlaczego nie przeszedłem całkowicie:
Gdzie bun test wygrywa:
- Szybkość startu.
bun testzaczyna wykonywać testy szybciej niż Vitest zdąży załadować swój config. - Zero konfiguracji. Żaden
vitest.config.tsnie jest potrzebny dla podstawowych setupów. - Wbudowany TypeScript. Żadnego kroku transformacji.
Gdzie Vitest wciąż wygrywa:
- Ekosystem: Vitest ma więcej pluginów, lepszą integrację z IDE i większą społeczność.
- Konfiguracja: System konfiguracji Vitest jest bardziej elastyczny. Własne reportery, złożone pliki setupu, wiele środowisk testowych.
- Tryb przeglądarkowy: Vitest może uruchamiać testy w prawdziwej przeglądarce. Bun nie może.
- Kompatybilność: Niektóre biblioteki testowe (Testing Library, MSW) zostały dokładniej przetestowane z Vitest/Jest.
- Snapshot testing: Oba to wspierają, ale implementacja Vitest jest bardziej dojrzała z lepszym wyjściem diff.
Dla nowego projektu z prostymi potrzebami testowymi użyłbym bun test. Dla ugruntowanego projektu z Testing Library, MSW i złożonym mockowaniem zostaję przy Vitest.
Bundler Bun#
bun build to szybki bundler JavaScript/TypeScript. To nie jest zamiennik webpacka — jest bardziej w kategorii esbuild: szybki, opiniowany i skupiony na typowych przypadkach.
Podstawowe bundlowanie#
# 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 programistyczne#
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 wspiera tree-shaking dla 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 outputGdzie Bun build wypada słabiej#
- Brak bundlowania CSS — Potrzebujesz osobnego narzędzia do CSS (PostCSS, Lightning CSS, Tailwind CLI).
- Brak generowania HTML — Bundluje JavaScript/TypeScript, nie pełne aplikacje webowe.
- Ekosystem pluginów — esbuild ma znacznie większy ekosystem pluginów. API pluginów Bun jest kompatybilne, ale społeczność jest mniejsza.
- Zaawansowany code splitting — Webpack i Rollup wciąż oferują bardziej wyrafinowane strategie chunków.
Do budowania biblioteki lub prostego bundla JS aplikacji webowej bun build jest doskonały. Do złożonych buildów aplikacji z CSS modules, optymalizacją obrazów i niestandardowymi strategiami chunków wciąż będziesz chciał pełny bundler.
Makra Bun#
Jedna naprawdę unikatowa funkcja: wykonywanie kodu w czasie kompilacji przez makra.
// 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}`);Po zbundlowaniu getBuildInfo() jest zastąpione literalnym obiektem — żadnego wywołania funkcji w runtime, żadnego importu child_process. Kod uruchomił się podczas buildu, a wynik został zinlineowany. To potężne do osadzania metadanych buildu, flag funkcjonalności czy konfiguracji specyficznej dla środowiska.
Używanie Bun z Next.js#
To pytanie, które dostaję najczęściej, więc pozwól, że będę bardzo precyzyjny.
Co działa dzisiaj#
Bun jako menedżer pakietów dla Next.js — działa idealnie:
# 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 startTo robię w każdym projekcie Next.js. Komenda bun run <script> czyta sekcję scripts z package.json i ją wykonuje. Domyślnie używa systemowego Node.js do faktycznego wykonania. Dostajesz szybką instalację pakietów Bun bez zmiany runtime'u.
Runtime Bun do rozwoju Next.js:
# Force Next.js to run under Bun's runtime
bun --bun run devTo działa w developmencie w większości przypadków. Flaga --bun mówi Bun, żeby użył własnego runtime'u zamiast delegowania do Node.js. Hot module replacement działa. API routes działają. Server components działają.
Co jest wciąż eksperymentalne#
Runtime Bun dla produkcyjnych buildów Next.js:
# Build with Bun runtime
bun --bun run build
# Start production server with Bun runtime
bun --bun run startTo działa dla wielu projektów, ale natrafiłem na przypadki brzegowe:
- Niektóre zachowania middleware się różnią — Jeśli używasz middleware Next.js, które zależy od API specyficznych dla Node.js, możesz natrafić na problemy z kompatybilnością.
- Optymalizacja obrazów — Pipeline optymalizacji obrazów Next.js używa sharp, który jest natywnym addonem. Działa z Bun, ale widziałem sporadyczne problemy.
- ISR (Incremental Static Regeneration) — Działa, ale nie przetestowałem tego pod obciążeniem z Bun na produkcji.
Moja rekomendacja dla Next.js#
Używaj Bun jako menedżera pakietów. Używaj Node.js jako runtime'u. To daje ci korzyści szybkości bun install bez jakiegokolwiek ryzyka kompatybilności.
{
"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.jsGdy kompatybilność Bun z Node.js osiągnie 100% dla wewnętrznego użycia Next.js (jest blisko, ale jeszcze nie tam), przejdę. Do tego czasu sam menedżer pakietów oszczędza mi wystarczająco dużo czasu, by uzasadnić instalację.
Docker z Bun#
Oficjalny obraz Docker Bun jest dobrze utrzymywany i gotowy na produkcję.
Podstawowy Dockerfile#
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 wieloetapowy dla minimalnego obrazu#
# 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"]Kompilacja do pojedynczego pliku binarnego#
To jedna z zabójczych funkcji Bun do wdrażania:
# 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"]Skompilowany plik binarny ma zwykle 50-90 MB (pakuje runtime Bun). To większe niż plik binarny Go, ale znacznie mniejsze niż pełna instalacja Node.js plus node_modules. Dla konteneryzowanych wdrożeń samowystarczalny charakter to znaczne uproszczenie.
Porównanie rozmiarów#
# 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_modulesPodejście z plikiem binarnym eliminuje node_modules całkowicie z finalnego obrazu. Żadnego npm install na produkcji. Żadnej powierzchni łańcucha dostaw od setek pakietów. Tylko jeden plik.
Wzorce migracji#
Jeśli rozważasz przejście na Bun, oto inkrementalna ścieżka, którą polecam:
Faza 1: Tylko menedżer pakietów (zero ryzyka)#
# Replace npm/yarn/pnpm with bun install
# Change your CI pipeline:
# Before:
npm ci
# After:
bun install --frozen-lockfileŻadnych zmian w kodzie. Żadnych zmian runtime'u. Tylko szybsze instalacje. Jeśli cokolwiek się zepsuje (nie zepsuje), cofnij usuwając bun.lockb i uruchamiając npm install.
Faza 2: Skrypty i narzędzia#
# 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.tsWciąż używasz Node.js jako runtime'u dla twojej właściwej aplikacji. Ale skrypty korzystają z szybszego startu Bun i natywnego wsparcia TypeScript.
Faza 3: Test runner (średnie ryzyko)#
# Replace vitest/jest with bun test for simple test suites
bun test
# Keep vitest for complex test setups
# (Testing Library, MSW, custom environments)Uruchom swój pełny zestaw testów pod bun test. Jeśli wszystko przechodzi, wyeliminowałeś devDependency. Jeśli niektóre testy nie przechodzą z powodu kompatybilności, zostaw Vitest dla tych i używaj bun test dla reszty.
Faza 4: Runtime dla nowych serwisów (skalkulowane ryzyko)#
// New microservices or APIs — start with Bun from day one
Bun.serve({
port: 3000,
fetch(req) {
// Your new service here
},
});Nie migruj istniejących serwisów Node.js na runtime Bun. Zamiast tego pisz nowe serwisy z Bun od początku. To ogranicza twój promień rażenia.
Faza 5: Migracja runtime'u (zaawansowane)#
# Only after thorough testing:
# Replace node with bun for existing services
# Before:
node dist/server.js
# After:
bun dist/server.jsPolecam to tylko dla serwisów z doskonałym pokryciem testami. Uruchom swoje testy obciążeniowe pod Bun zanim przełączysz produkcję.
Zmienne środowiskowe i konfiguracja#
Bun obsługuje pliki .env automatycznie — nie potrzebujesz pakietu dotenv:
# .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 ładuje .env, .env.local, .env.production itp. automatycznie, podążając za tą samą konwencją co Next.js. Jedna zależność mniej w twoim package.json.
Obsługa błędów i debugowanie#
Wyjście błędów Bun znacząco się poprawiło, ale wciąż nie jest tak dopracowane jak w Node.js w niektórych przypadkach:
# 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.tsDla VS Code dodaj to do swojego .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 trace'y w Bun są ogólnie dokładne i zawierają source mapy dla TypeScript. Główna luka w debugowaniu polega na tym, że niektóre narzędzia debugowania specyficzne dla Node.js (jak ndb czy clinic.js) nie działają z Bun.
Kwestie bezpieczeństwa#
Kilka rzeczy do przemyślenia, jeśli oceniasz Bun na produkcję:
Dojrzałość: Node.js jest na produkcji od 15+ lat. Każdy przypadek brzegowy w parsowaniu HTTP, obsłudze TLS i przetwarzaniu strumieni został znaleziony i naprawiony. Bun jest młodszy. Jest dobrze przetestowany, ale powierzchnia dla nieodkrytych błędów jest większa.
Łatki bezpieczeństwa: Zespół Bun wydaje aktualizacje często, ale zespół bezpieczeństwa Node.js ma formalny proces CVE, skoordynowane ujawnianie i dłuższy track record. Dla aplikacji krytycznych pod względem bezpieczeństwa to ma znaczenie.
Łańcuch dostaw: Wbudowane funkcje Bun (SQLite, serwer HTTP, WebSockety) oznaczają mniej zależności npm. Mniej zależności oznacza mniejszą powierzchnię ataku na łańcuch dostaw. To prawdziwa zaleta bezpieczeństwa.
# 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)To znacząca redukcja liczby pakietów, którym ufasz w swoim produkcyjnym obciążeniu.
Tuning wydajności#
Kilka wskazówek wydajnościowych specyficznych dla 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 limitTypowe pułapki#
Po roku używania Bun, oto rzeczy, które mnie potknęły:
1. Zachowanie globalnego fetch się różni#
// 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. Zachowanie wyjścia procesu#
// 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. Konfiguracja 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 w developmencie#
# 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. Plik konfiguracyjny 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"Mój werdykt#
Po roku użycia na produkcji, oto gdzie się ustabilizowałem:
Gdzie używam Bun dzisiaj#
Menedżer pakietów we wszystkich projektach — w tym ten blog na Next.js. bun install jest szybszy, a kompatybilność jest zasadniczo idealna. Nie widzę powodu, żeby dalej używać npm czy yarn. pnpm to jedyna alternatywa, którą bym rozważył (za jego rygorystyczne rozwiązywanie zależności w monorepo).
Runtime dla skryptów i narzędzi CLI — Każdy plik TypeScript, który muszę uruchomić jednorazowo, uruchamiam z bun. Żadnego kroku kompilacji. Szybki start. Wbudowane ładowanie .env. Całkowicie zastąpił ts-node i tsx w moim workflow.
Runtime dla małych API i narzędzi wewnętrznych — Bun.serve() + bun:sqlite to niesamowicie produktywny stos dla narzędzi wewnętrznych, handlerów webhooków i małych serwisów. Model wdrożenia „jeden plik binarny, zero zależności" jest przekonujący.
Test runner dla prostych projektów — Dla projektów z prostymi potrzebami testowymi bun test jest szybki i nie wymaga żadnej konfiguracji.
Gdzie zostaję przy Node.js#
Produkcyjny Next.js — Nie dlatego, że Bun nie działa, ale dlatego, że stosunek ryzyka do zysku jeszcze tego nie uzasadnia. Next.js to złożony framework z wieloma punktami integracji. Chcę pod nim najbardziej sprawdzony w boju runtime.
Krytyczne serwisy produkcyjne — Moje główne serwery API działają na Node.js za PM2. Ekosystem monitoringu, narzędzia debugowania, wiedza operacyjna — to wszystko Node.js. Bun tam dojdzie, ale jeszcze nie jest tam.
Cokolwiek z natywnymi addonami — Jeśli łańcuch zależności zawiera natywne addony C++, nawet nie próbuję Bun. Nie warte debugowania problemów z kompatybilnością.
Zespoły, które nie znają Bun — Wprowadzanie Bun jako runtime'u do zespołu, który nigdy go nie używał, dodaje narzut kognitywny. Jako menedżer pakietów, ok. Jako runtime, poczekaj aż zespół będzie gotowy.
Na co patrzę#
Tracker kompatybilności Bun — Gdy osiągnie 100% dla API Node.js, na których mi zależy, dokonam ponownej oceny.
Wsparcie frameworków — Next.js, Remix i SvelteKit mają różne poziomy wsparcia Bun. Gdy któryś z nich oficjalnie wesprze Bun jako produkcyjny runtime, to sygnał.
Adopcja enterprise — Gdy firmy z realnymi SLA będą uruchamiać Bun na produkcji i pisać o tym, kwestia dojrzałości zostanie rozwiązana.
Linia wydań 1.2+ — Bun rozwija się szybko. Nowe funkcje pojawiają się co tydzień. Bun, którego używam dzisiaj, jest znacząco lepszy niż Bun, którego próbowałem rok temu.
Podsumowanie#
Bun nie jest srebrną kulą. Nie sprawi, że wolna aplikacja stanie się szybka i nie sprawi, że źle zaprojektowane API stanie się dobrze zaprojektowane. Ale jest prawdziwą poprawą doświadczenia deweloperskiego w ekosystemie JavaScript.
Rzecz, którą najbardziej cenię w Bun, to nie żadna pojedyncza funkcja. To redukcja złożoności toolchaina. Jeden plik binarny, który instaluje pakiety, uruchamia TypeScript, bundluje kod i uruchamia testy. Żadnego tsconfig.json dla skryptów. Żadnego Babel. Żadnej osobnej konfiguracji test runnera. Tylko bun run your-file.ts i działa.
Praktyczna rada: zacznij od bun install. Zero ryzyka, natychmiastowa korzyść. Potem spróbuj bun run do skryptów. Potem oceń resztę na podstawie swoich konkretnych potrzeb. Nie musisz iść na całość. Bun działa idealnie jako częściowy zamiennik i tak prawdopodobnie powinno go używać większość ludzi dzisiaj.
Krajobraz runtime'ów JavaScript jest lepszy z Bun w nim. Konkurencja sprawia, że Node.js też staje się lepszy — Node.js 22+ stał się znacząco szybszy, częściowo w odpowiedzi na presję Bun. Wszyscy wygrywają.