Bun у продакшені: Що працює, що ні, і що мене здивувало
Bun як рантайм, пакетний менеджер, бандлер та тест-раннер. Реальні бенчмарки, прогалини сумісності з Node.js, патерни міграції та де я використовую Bun у продакшені сьогодні.
Кожні кілька років в екосистемі JavaScript з'являється новий рантайм, і дискурс іде за передбачуваною дугою. Хайп. Бенчмарки. "X мертвий." Перевірка реальністю. Усталення у фактичних випадках використання, де новий інструмент справді блищить.
Bun зараз посередині цієї дуги. І на відміну від більшості претендентів, він залишається. Не тому, що він "швидший" (хоча часто це так), а тому, що він вирішує справді іншу проблему: ланцюжок інструментів JavaScript має забагато рухомих частин, і Bun зводить їх в одне ціле.
Я використовую Bun у різних якостях вже понад рік. Дещо в продакшені. Дещо замінюючи інструменти, які я думав, що ніколи не замінюватиму. Цей пост — чесний звіт про те, що працює, що ні, і де прогалини досі мають значення.
Що таке Bun насправді#
Перше хибне уявлення, яке треба розвіяти: Bun — це не "швидший Node.js." Таке формулювання його недооцінює.
Bun — це чотири інструменти в одному бінарнику:
- Рантайм JavaScript/TypeScript — виконує ваш код, як Node.js або Deno
- Пакетний менеджер — замінює npm, yarn або pnpm
- Бандлер — замінює esbuild, webpack або Rollup для певних випадків використання
- Тест-раннер — замінює Jest або Vitest для більшості тестових наборів
Ключова архітектурна відмінність від Node.js — це рушій. Node.js використовує V8 (рушій Chrome). Bun використовує JavaScriptCore (рушій Safari). Обидва — зрілі, готові до продакшену рушії, але вони роблять різні компроміси. JavaScriptCore має тенденцію до швидшого запуску та нижчих накладних витрат на пам'ять. V8 має тенденцію до кращої пікової пропускної здатності для тривалих обчислень. На практиці ці відмінності менші, ніж ви думаєте, для більшості робочих навантажень.
Інший основний диференціатор: Bun написаний на Zig — мові системного програмування, яка знаходиться приблизно на тому ж рівні, що і C, але з кращими гарантіями безпеки пам'яті. Ось чому Bun може бути таким агресивним щодо продуктивності — Zig дає вам такий самий низькорівневий контроль, як C, без такої щільності пасток.
# Перевірте версію Bun
bun --version
# Запустіть TypeScript файл напряму — без tsconfig, без кроку компіляції
bun run server.ts
# Встановіть пакети
bun install
# Запустіть тести
bun test
# Зберіть для продакшену
bun build ./src/index.ts --outdir ./distЦе один бінарник, який виконує роботу node + npm + esbuild + vitest. Подобається вам це чи ні, це переконливе зменшення складності.
Заявки про швидкість — чесні бенчмарки#
Скажу прямо: маркетингові бенчмарки Bun вибіркові. Не шахрайські — вибіркові. Вони показують сценарії, де Bun працює найкраще, що саме те, чого ви очікуєте від маркетингових матеріалів. Проблема в тому, що люди екстраполюють з цих бенчмарків і стверджують, що Bun "у 25 разів швидший" у всьому, що абсолютно не так.
Ось де Bun справді, суттєво швидший:
Час запуску#
Це найбільша справжня перевага Bun, і навіть близько немає конкурентів.
# Вимірювання часу запуску — запустити кожен 100 разів
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# Типові результати:
# node: ~40мс
# bun: ~6мсЦе приблизно 6-7-кратна різниця в часі запуску. Для скриптів, CLI-інструментів і безсерверних функцій, де має значення холодний старт, це суттєво. Для довготривалого серверного процесу, який запускається раз і працює тижнями, це несуттєво.
Встановлення пакетів#
Це інша область, де Bun ставить конкурентів у незручне становище.
# Бенчмарк чистого встановлення — спершу видаліть node_modules і lockfile
rm -rf node_modules bun.lockb package-lock.json
# Час npm
time npm install
# Real: ~18.4с (типовий проєкт середнього розміру)
# Час bun
time bun install
# Real: ~2.1сЦе 8-9-кратна різниця, і вона стабільна. Причини переважно такі:
- Бінарний lockfile —
bun.lockbмає бінарний формат, не JSON. Швидше читати і записувати. - Глобальний кеш — Bun підтримує глобальний кеш модулів, тому повторні встановлення в різних проєктах використовують спільні завантажені пакети.
- I/O на Zig — Сам пакетний менеджер написаний на Zig, не на JavaScript. Операції файлового вводу-виводу ближчі до заліза.
- Стратегія симлінків — Bun використовує жорсткі посилання з глобального кешу замість копіювання файлів.
Пропускна здатність HTTP-сервера#
Вбудований HTTP-сервер Bun швидкий, але порівняння потребують контексту.
# Швидкий і брудний бенчмарк з bombardier
# Тестування простої відповіді "Hello World"
# Bun сервер
bombardier -c 100 -d 10s http://localhost:3000
# Запитів/сек: ~105,000
# Node.js (нативний модуль http)
bombardier -c 100 -d 10s http://localhost:3001
# Запитів/сек: ~48,000
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# Запитів/сек: ~15,000Bun проти чистого Node.js: приблизно 2x для тривіальних відповідей. Bun проти Express: приблизно 7x, але це несправедливо, бо Express додає накладні витрати мідлверу. У момент, коли ви додаєте реальну логіку — запити до бази даних, автентифікацію, серіалізацію JSON реальних даних — розрив різко скорочується.
Де різниця незначна#
Обчислення, обмежені CPU:
// fibonacci.ts — це обмежено рушієм, а не рантаймом
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)}мс`);bun run fibonacci.ts # ~1650мс
node fibonacci.ts # ~1580мсNode.js (V8) тут насправді трохи виграє. JIT-компілятор V8 агресивніший на гарячих циклах. Для обчислень, обмежених CPU, різниця рушіїв — це нічия: іноді виграє V8, іноді JSC, і різниця в межах шуму.
Як запускати власні бенчмарки#
Не довіряйте нічиїм бенчмаркам, включаючи мої. Ось як виміряти те, що важливо для вашого конкретного робочого навантаження:
# Встановіть hyperfine для правильного бенчмаркінгу
brew install hyperfine # macOS
# або: cargo install hyperfine
# Бенчмарк запуску + виконання вашого реального застосунку
hyperfine --warmup 3 \
'node dist/server.js' \
'bun src/server.ts' \
--prepare 'sleep 0.1'
# Для HTTP-серверів використовуйте bombardier або wrk
# Важливо: тестуйте з реалістичними навантаженнями, а не "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
# Порівняння пам'яті
/usr/bin/time -v node server.js # Linux
/usr/bin/time -l bun server.ts # macOSПравило великого пальця: якщо ваше вузьке місце — I/O (файлова система, мережа, база даних), перевага Bun скромна. Якщо ваше вузьке місце — час запуску або швидкість ланцюжка інструментів, Bun виграє значно. Якщо ваше вузьке місце — чисті обчислення, це нічия.
Bun як пакетний менеджер#
Ось де я повністю перейшов. Навіть у проєктах, де я запускаю Node.js у продакшені, я використовую bun install для локальної розробки та CI. Він просто швидший, і сумісність відмінна.
Основи#
# Встановити всі залежності з package.json
bun install
# Додати залежність
bun add express
# Додати dev-залежність
bun add -d vitest
# Видалити залежність
bun remove express
# Оновити залежність
bun update express
# Встановити конкретну версію
bun add express@4.18.2Якщо ви використовували npm або yarn, це повністю знайоме. Прапори трохи відрізняються (-d замість --save-dev), але ментальна модель ідентична.
Ситуація з lockfile#
Bun використовує bun.lockb — бінарний lockfile. Це одночасно його суперсила і найбільша точка тертя.
Добре: він драматично швидший для читання і запису. Бінарний формат означає, що Bun може розпарсити lockfile за мікросекунди, а не за сотні мілісекунд, які npm витрачає на парсинг package-lock.json.
Погано: ви не можете переглянути його в diff. Якщо ви в команді і хтось оновлює залежність, ви не можете подивитися diff lockfile в PR і побачити, що змінилося. Це має більше значення, ніж адвокати швидкості хочуть визнати.
# Ви можете вивантажити lockfile у читабельний формат
bun bun.lockb > lockfile-dump.txt
# Або використати вбудований текстовий вивід
bun install --yarn
# Це генерує yarn.lock поряд з bun.lockbМій підхід: я комічу bun.lockb в репозиторій і також генерую yarn.lock або package-lock.json як читабельний запасний варіант. Подвійна страховка.
Підтримка воркспейсів#
Bun підтримує воркспейси у стилі npm/yarn:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# Встановити залежності для всіх воркспейсів
bun install
# Запустити скрипт у конкретному воркспейсі
bun run --filter packages/shared build
# Додати залежність до конкретного воркспейсу
bun add react --filter apps/webПідтримка воркспейсів надійна і значно покращилася. Основна прогалина порівняно з pnpm полягає в тому, що розв'язання залежностей воркспейсів у Bun менш суворе — суворість pnpm є перевагою для монорепозиторіїв, оскільки вона ловить фантомні залежності.
Сумісність з існуючими проєктами#
Ви можете підставити bun install практично в будь-який існуючий проєкт Node.js. Він читає package.json, поважає .npmrc для конфігурації реєстру та правильно обробляє peerDependencies. Перехід зазвичай такий:
# Крок 1: Видалити існуючий lockfile та node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
# Крок 2: Встановити з Bun
bun install
# Крок 3: Перевірити, що ваш застосунок все ще працює
bun run dev
# або: node dist/server.js (Bun пакетний менеджер, Node рантайм)Я робив це на десятку проєктів і мав нуль проблем із самим пакетним менеджером. Єдиний підводний камінь — якщо ваш CI-пайплайн конкретно шукає package-lock.json — вам потрібно буде оновити його для обробки bun.lockb.
Сумісність з Node.js#
Це розділ, де я маю бути найобережнішим, тому що ситуація змінюється кожного місяця. Станом на початок 2026 року, ось чесна картина.
Що працює#
Переважна більшість npm-пакетів працює без модифікацій. Bun реалізує більшість вбудованих модулів Node.js:
// Все це працює як очікується в 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";Працюють і CommonJS, і ESM. require() та import можуть співіснувати. TypeScript запускається без будь-якого кроку компіляції — Bun знімає типи на етапі парсингу.
Фреймворки, що працюють:
- Express — працює, включаючи екосистему мідлверів
- Fastify — працює
- Hono — працює (і відмінно поєднується з Bun)
- Next.js — працює із застереженнями (докладніше нижче)
- Prisma — працює
- Drizzle ORM — працює
- Socket.io — працює
Що не працює (або має проблеми)#
Прогалини зазвичай потрапляють у кілька категорій:
Нативні аддони (node-gyp): якщо пакет використовує C++-аддони, скомпільовані з node-gyp, він може не працювати з Bun. Bun має власну систему FFI і підтримує багато нативних модулів, але покриття не 100%. Наприклад, bcrypt (нативний) мав проблеми — використовуйте bcryptjs замість нього.
# Перевірте, чи пакет використовує нативні аддони
ls node_modules/your-package/binding.gyp # Якщо це існує, він нативнийСпецифічні внутрішні API Node.js: деякі пакети звертаються до внутрішніх API Node.js, таких як process.binding(), або використовують V8-специфічні API. Вони не працюватимуть у Bun, оскільки він працює на JavaScriptCore.
// Це НЕ працюватиме в Bun — специфічне для V8
const v8 = require("v8");
v8.serialize({ data: "test" });
// Це ПРАЦЮВАТИМЕ — використовуйте еквівалент Bun або крос-рантайм підхід
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));Worker threads: Bun підтримує Web Workers та node:worker_threads, але є граничні випадки. Деякі розширені патерни використання — особливо навколо SharedArrayBuffer і Atomics — можуть поводитися по-різному.
Модуль vm: node:vm має часткову підтримку. Якщо ваш код або залежність активно використовує vm.createContext() (деякі шаблонізатори це роблять), тестуйте ретельно.
Трекер сумісності#
Bun підтримує офіційний трекер сумісності. Перевірте його перед тим, як обирати Bun для проєкту:
# Запустіть вбудовану перевірку сумісності Bun для вашого проєкту
bun --bun node_modules/.bin/your-tool
# Прапор --bun примусово використовує рантайм Bun навіть для скриптів node_modulesМоя рекомендація: не припускайте сумісність. Запустіть свій тестовий набір під Bun перед прийняттям рішення. Це займає п'ять хвилин і економить години відладки.
# Швидка перевірка сумісності — запустіть повний тестовий набір під Bun
bun test # Якщо ви використовуєте тест-раннер bun
# або
bun run vitest # Якщо ви використовуєте vitestВбудовані API Bun#
Ось де Bun стає цікавим. Замість того, щоб просто перереалізовувати API Node.js, Bun надає власні API, розроблені для простоти та швидкості.
Bun.serve() — вбудований HTTP-сервер#
Це API, який я використовую найчастіше. Він чистий, швидкий, і підтримка WebSocket вбудована прямо в нього.
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}`);Кілька речей, на які варто звернути увагу:
- Веб-стандартні Request/Response — жодного пропрієтарного API. Обробник
fetchотримує стандартнийRequestі повертає стандартнийResponse. Якщо ви писали Cloudflare Worker, це відчувається ідентично. Response.json()— вбудований помічник для JSON-відповідей.- Не потрібен імпорт —
Bun.serveє глобальним. Безrequire("http").
Ось більш реалістичний приклад з маршрутизацією, парсингом JSON-тіла та обробкою помилок:
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}`);Це повний CRUD API з SQLite приблизно в 50 рядках. Без Express, без ORM, без ланцюжка мідлверів. Для невеликих API та внутрішніх інструментів це тепер мій основний підхід.
Bun.file() та Bun.write() — файловий I/O#
Файловий API Bun освіжаюче простий порівняно з fs.readFile():
// Читання файлів
const file = Bun.file("./config.json");
const text = await file.text(); // Читати як рядок
const json = await file.json(); // Парсити як JSON напряму
const bytes = await file.arrayBuffer(); // Читати як ArrayBuffer
const stream = file.stream(); // Читати як ReadableStream
// Метадані файлу
console.log(file.size); // Розмір у байтах
console.log(file.type); // MIME-тип (наприклад, "application/json")
// Запис файлів
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"));
// Записати тіло Response у файл
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);API Bun.file() є лінивим — він не читає файл, доки ви не викличете .text(), .json() тощо. Це означає, що ви можете передавати посилання Bun.file() без витрат на I/O, поки вам фактично не знадобляться дані.
Вбудована підтримка WebSocket#
WebSocket — першокласний громадянин у 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) {
// Розсилка всім підписникам
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`Client disconnected: ${ws.data.userId}`);
ws.unsubscribe("chat");
},
},
});Патерн server.publish() і ws.subscribe() — це вбудований pub/sub. Без Redis, без окремої бібліотеки WebSocket. Для простих функцій реального часу це неймовірно зручно.
Вбудований SQLite з bun:sqlite#
Це здивувало мене найбільше. Bun поставляється з SQLite, вбудованим прямо в рантайм:
import { Database } from "bun:sqlite";
// Відкрити або створити базу даних
const db = new Database("myapp.db");
// Режим WAL для кращої продуктивності конкурентного читання
db.exec("PRAGMA journal_mode = WAL");
// Створити таблиці
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'))
)
`);
// Підготовлені запити (багаторазові, швидші для повторних запитів)
const insertUser = db.prepare(
"INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = $email"
);
// Використання
const user = insertUser.get({
$email: "alice@example.com",
$name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
// Транзакції
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`);Це синхронний SQLite з продуктивністю C-бібліотеки (тому що він нею і є — Bun вбудовує libsqlite3 напряму). Для CLI-інструментів, local-first застосунків та невеликих сервісів, вбудований SQLite означає нуль зовнішніх залежностей для вашого рівня даних.
Тест-раннер Bun#
bun test — це drop-in заміна для Jest у більшості випадків. Він використовує той самий API describe/it/expect і підтримує більшість матчерів 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);
});
});# Запустити всі тести
bun test
# Запустити конкретний файл
bun test math.test.ts
# Запустити тести за патерном
bun test --test-name-pattern "adds numbers"
# Режим спостереження
bun test --watch
# Покриття
bun test --coverageМокінг#
Bun підтримує Jest-сумісний мокінг:
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
// Мок модуля
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");
});
});
// Шпигунство за методом об'єкта
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 проти Vitest — моє чесне порівняння#
Я використовую Vitest для цього проєкту (і більшості моїх проєктів). Ось чому я не перейшов повністю:
Де bun test виграє:
- Швидкість запуску.
bun testпочинає виконувати тести швидше, ніж Vitest встигає завершити завантаження конфігурації. - Нульова конфігурація. Не потрібен
vitest.config.tsдля базових налаштувань. - Вбудований TypeScript. Без кроку трансформації.
Де Vitest все ще виграє:
- Екосистема: Vitest має більше плагінів, кращу інтеграцію з IDE та більшу спільноту.
- Конфігурація: Система конфігурації Vitest гнучкіша. Кастомні репортери, складні файли налаштувань, кілька тестових середовищ.
- Браузерний режим: Vitest може запускати тести в реальному браузері. Bun — ні.
- Сумісність: Деякі тестові бібліотеки (Testing Library, MSW) були більш ретельно протестовані з Vitest/Jest.
- Тестування знімків: Обидва підтримують, але реалізація Vitest більш зріла з кращим виводом diff.
Для нового проєкту з простими потребами тестування я б використав bun test. Для усталеного проєкту з Testing Library, MSW та складним мокінгом я залишаюся з Vitest.
Бандлер Bun#
bun build — це швидкий бандлер JavaScript/TypeScript. Це не заміна webpack — він більше в категорії esbuild: швидкий, з власною думкою та зосереджений на типових випадках.
Базове збирання#
# Зібрати одну точку входу
bun build ./src/index.ts --outdir ./dist
# Зібрати для різних цілей
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
# Мініфікація
bun build ./src/index.ts --outdir ./dist --minify
# Генерація sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap externalПрограмний API#
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, // Розділення коду
sourcemap: "external",
external: ["react", "react-dom"], // Не збирати ці
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 підтримує tree-shaking для 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
# Функція `unused` не з'явиться у виводіДе Bun Build програє#
- Без збирання CSS — вам потрібен окремий інструмент для CSS (PostCSS, Lightning CSS, Tailwind CLI).
- Без генерації HTML — він збирає JavaScript/TypeScript, не повноцінні веб-застосунки.
- Екосистема плагінів — esbuild має значно більшу екосистему плагінів. API плагінів Bun сумісний, але спільнота менша.
- Просунуте розділення коду — Webpack і Rollup все ще пропонують більш витончені стратегії чанків.
Для збирання бібліотеки або простого JS-бандлу веб-застосунку bun build відмінний. Для складних збірок застосунків з CSS-модулями, оптимізацією зображень та кастомними стратегіями чанків вам все ще потрібен повноцінний бандлер.
Макроси Bun#
Одна справді унікальна функція: виконання коду на етапі компіляції через макроси.
// build-info.ts — цей файл виконується ПІД ЧАС ЗБИРАННЯ, а не в рантаймі
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() виконується під час збирання
// Результат вставляється як статичне значення
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);Після збирання getBuildInfo() замінюється на літеральний об'єкт — жодного виклику функції в рантаймі, жодного імпорту child_process. Код виконався під час збирання, і результат був вбудований. Це потужно для вбудовування метаданих збирання, прапорів функцій або конфігурації, специфічної для середовища.
Використання Bun з Next.js#
Це питання, яке мені задають найчастіше, тому дозвольте бути дуже конкретним.
Що працює сьогодні#
Bun як пакетний менеджер для Next.js — працює ідеально:
# Використовуйте Bun для встановлення залежностей, потім використовуйте Node.js для запуску Next.js
bun install
bun run dev # Це фактично запускає скрипт "dev" через Node.js за замовчуванням
bun run build
bun run startЦе те, що я роблю для кожного проєкту Next.js. Команда bun run <script> читає секцію scripts файлу package.json і виконує її. За замовчуванням вона використовує системний Node.js для фактичного виконання. Ви отримуєте швидке встановлення пакетів Bun без зміни рантайму.
Рантайм Bun для розробки Next.js:
# Примусово запустити Next.js під рантаймом Bun
bun --bun run devЦе працює для розробки в більшості випадків. Прапор --bun вказує Bun використовувати власний рантайм замість делегування Node.js. Гаряча заміна модулів працює. API-маршрути працюють. Серверні компоненти працюють.
Що все ще експериментальне#
Рантайм Bun для продакшн-збірок Next.js:
# Збирання з рантаймом Bun
bun --bun run build
# Запуск продакшн-сервера з рантаймом Bun
bun --bun run startЦе працює для багатьох проєктів, але я зустрічав граничні випадки:
- Деяка поведінка мідлверів відрізняється — якщо ви використовуєте мідлвер Next.js, який залежить від Node.js-специфічних API, ви можете зіткнутися з проблемами сумісності.
- Оптимізація зображень — пайплайн оптимізації зображень Next.js використовує sharp, який є нативним аддоном. Він працює з Bun, але я бачив випадкові проблеми.
- ISR (Incremental Static Regeneration) — працює, але я не стрес-тестував його під Bun у продакшені.
Моя рекомендація для Next.js#
Використовуйте Bun як пакетний менеджер. Використовуйте Node.js як рантайм. Це дає вам переваги швидкості bun install без будь-якого ризику сумісності.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# Щоденний робочий процес
bun install # Швидке встановлення пакетів
bun run dev # Запускає "next dev" через Node.js
bun run build # Запускає "next build" через Node.jsКоли сумісність Bun з Node.js досягне 100% для внутрішнього використання Next.js (це близько, але ще не там), я перейду. До того часу один лише пакетний менеджер економить мені достатньо часу, щоб виправдати встановлення.
Docker з Bun#
Офіційний Docker-образ Bun добре підтримується і готовий до продакшену.
Базовий Dockerfile#
FROM oven/bun:1 AS base
WORKDIR /app
# Встановлення залежностей
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Збирання (за потреби)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Продакшн
FROM base AS production
WORKDIR /app
# Не запускати від 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"]Багатоетапна збірка для мінімального образу#
# Етап збирання: повний образ Bun з усіма залежностями
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
# Етап рантайму: менший базовий образ
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"]Компіляція в один бінарник#
Це одна з кілер-функцій Bun для деплойменту:
# Компілювати ваш застосунок в єдиний виконуваний файл
bun build --compile ./src/server.ts --outfile server
# Вивід — це автономний бінарник — не потрібен Bun або Node.js для запуску
./server# Ультра-мінімальний Docker-образ з використанням скомпільованого бінарника
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
# Фінальний образ — лише бінарник
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"]Скомпільований бінарник зазвичай має розмір 50-90 МБ (він включає рантайм Bun). Це більше, ніж бінарник Go, але набагато менше, ніж повна інсталяція Node.js плюс node_modules. Для контейнеризованих деплойментів автономна природа — це значне спрощення.
Порівняння розмірів#
# Образ Node.js
docker images | grep node
# node:20-slim ~180МБ
# Образ Bun
docker images | grep bun
# oven/bun:1-slim ~130МБ
# Скомпільований бінарник на debian:bookworm-slim
# ~80МБ база + ~70МБ бінарник = ~150МБ загалом
# проти Alpine з Node.js
# node:20-alpine ~130МБ + node_modulesПідхід з бінарником повністю виключає node_modules з фінального образу. Жодного npm install у продакшені. Жодної поверхні для атак на ланцюжок постачання від сотень пакетів. Лише один файл.
Патерни міграції#
Якщо ви розглядаєте перехід на Bun, ось покроковий шлях, який я рекомендую:
Фаза 1: Лише пакетний менеджер (нульовий ризик)#
# Замінити npm/yarn/pnpm на bun install
# Змініть свій CI-пайплайн:
# Було:
npm ci
# Стало:
bun install --frozen-lockfileЖодних змін коду. Жодних змін рантайму. Просто швидші встановлення. Якщо щось зламається (не зламається), поверніться, видаливши bun.lockb і запустивши npm install.
Фаза 2: Скрипти та інструменти#
# Використовуйте bun для запуску скриптів розробки
bun run dev
bun run lint
bun run format
# Використовуйте bun для одноразових скриптів
bun run scripts/seed-database.ts
bun run scripts/migrate.tsВсе ще використовуємо Node.js як рантайм для вашого фактичного застосунку. Але скрипти виграють від швидшого запуску Bun та нативної підтримки TypeScript.
Фаза 3: Тест-раннер (середній ризик)#
# Замінити vitest/jest на bun test для простих тестових наборів
bun test
# Залишити vitest для складних тестових налаштувань
# (Testing Library, MSW, кастомні середовища)Запустіть повний тестовий набір під bun test. Якщо все проходить, ви усунули devDependency. Якщо деякі тести провалюються через сумісність, залиште Vitest для них і використовуйте bun test для решти.
Фаза 4: Рантайм для нових сервісів (зважений ризик)#
// Нові мікросервіси або API — починайте з Bun з першого дня
Bun.serve({
port: 3000,
fetch(req) {
// Ваш новий сервіс тут
},
});Не мігруйте існуючі сервіси Node.js на рантайм Bun. Натомість пишіть нові сервіси з Bun з самого початку. Це обмежує вашу зону ураження.
Фаза 5: Міграція рантайму (просунутий рівень)#
# Тільки після ретельного тестування:
# Замінити node на bun для існуючих сервісів
# Було:
node dist/server.js
# Стало:
bun dist/server.jsЯ рекомендую це лише для сервісів з відмінним покриттям тестами. Запустіть навантажувальні тести під Bun перед перемиканням продакшену.
Змінні середовища та конфігурація#
Bun автоматично обробляє файли .env — пакет dotenv не потрібен:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// Вони доступні без будь-якого імпорту
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun-специфічна альтернативаBun автоматично завантажує .env, .env.local, .env.production тощо, дотримуючись тієї ж конвенції, що й Next.js. На одну залежність менше у вашому package.json.
Обробка помилок та дебагінг#
Вивід помилок Bun значно покращився, але він все ще не такий відполірований, як у Node.js у деяких випадках:
# Дебагер Bun — працює з VS Code
bun --inspect run server.ts
# Bun inspect-brk — пауза на першому рядку
bun --inspect-brk run server.tsДля VS Code додайте це до вашого .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
}
]
}Стек-трейси в Bun загалом точні і включають source maps для TypeScript. Основна прогалина в дебагінгу — деякі Node.js-специфічні інструменти відладки (як ndb або clinic.js) не працюють з Bun.
Міркування щодо безпеки#
Кілька речей, про які варто подумати, якщо ви оцінюєте Bun для продакшену:
Зрілість: Node.js у продакшені вже понад 15 років. Кожен граничний випадок у парсингу HTTP, обробці TLS та стрімах був знайдений і виправлений. Bun молодший. Він добре протестований, але поверхня для невідкритих багів більша.
Патчі безпеки: Команда Bun часто випускає оновлення, але команда безпеки Node.js має формальний процес CVE, координоване розкриття та довший послужний список. Для критично важливих з точки зору безпеки застосунків це має значення.
Ланцюжок постачання: Вбудовані функції Bun (SQLite, HTTP-сервер, WebSocket) означають менше npm-залежностей. Менше залежностей означає меншу поверхню для атак на ланцюжок постачання. Це справжня перевага безпеки.
# Порівняння кількості залежностей
# Типовий проєкт Express + SQLite + WebSocket:
npm ls --all | wc -l
# ~340 пакетів
# Та сама функціональність з вбудованими можливостями Bun:
bun pm ls --all | wc -l
# ~12 пакетів (лише код вашого застосунку)Це значне зменшення кількості пакетів, яким ви довіряєте своє продакшн-навантаження.
Налаштування продуктивності#
Кілька Bun-специфічних порад щодо продуктивності:
// Використовуйте опції Bun.serve() для продакшн-налаштування
Bun.serve({
port: 3000,
// Збільшити максимальний розмір тіла запиту (за замовчуванням 128МБ)
maxRequestBodySize: 1024 * 1024 * 50, // 50МБ
// Увімкнути режим розробки для кращих сторінок помилок
development: process.env.NODE_ENV !== "production",
// Повторне використання порту (корисно для перезапусків без простою)
reusePort: true,
fetch(req) {
return new Response("OK");
},
});// Використовуйте Bun.Transpiler для трансформації коду в рантаймі
const transpiler = new Bun.Transpiler({
loader: "tsx",
target: "browser",
});
const code = transpiler.transformSync(`
const App: React.FC = () => <div>Hello</div>;
export default App;
`);# Прапори використання пам'яті Bun
bun --smol run server.ts # Зменшити відбиток пам'яті (трохи повільніше)
# Встановити максимальний розмір кучі
BUN_JSC_forceRAMSize=512000000 bun run server.ts # ~512МБ лімітПоширені підводні камені#
Після року використання Bun, ось речі, які мене підвели:
1. Поведінка глобального Fetch відрізняється#
// Node.js 18+ fetch і fetch Bun трохи відрізняються
// у тому, як вони обробляють певні заголовки та перенаправлення
// Bun слідує за перенаправленнями за замовчуванням (як браузери)
// Node.js fetch також слідує за перенаправленнями, але поведінка
// з певними кодами статусу (303, 307, 308) може відрізнятися
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // Будьте явними щодо обробки перенаправлень
});2. Поведінка завершення процесу#
// Bun завершується, коли цикл подій порожній
// Node.js іноді продовжує працювати через залишкові хендли
// Якщо ваш скрипт Bun несподівано завершується, щось не
// тримає цикл подій активним
// Це завершиться негайно в Bun:
setTimeout(() => {}, 0);
// Це продовжить працювати:
setTimeout(() => {}, 1000);
// (Bun завершується після спрацювання таймауту)3. Конфігурація TypeScript#
// Bun має власні значення за замовчуванням для tsconfig
// Якщо ви ділите проєкт між Bun і Node.js,
// будьте явними у вашому tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Додати визначення типів Bun
}
}# Встановити типи Bun
bun add -d @types/bun4. Гаряче перезавантаження в розробці#
# Bun має вбудований режим спостереження
bun --watch run server.ts
# Це перезапускає процес при змінах файлів
# Це не HMR (Hot Module Replacement) — це повний перезапуск
# Але оскільки Bun запускається так швидко, це відчувається миттєво5. Конфігураційний файл bunfig.toml#
# bunfig.toml — конфігураційний файл Bun (необов'язковий)
[install]
# Використовувати приватний реєстр
registry = "https://npm.mycompany.com"
# Реєстри з областю видимості
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
[test]
# Конфігурація тестів
coverage = true
coverageReporter = ["text", "lcov"]
[run]
# Оболонка для bun run
shell = "bash"Мій вердикт#
Після року використання у продакшені, ось де я зупинився:
Де я використовую Bun сьогодні#
Пакетний менеджер для всіх проєктів — включаючи цей блог на Next.js. bun install швидший, і сумісність по суті ідеальна. Я не бачу причин використовувати npm або yarn далі. pnpm — єдина альтернатива, яку б я розглядав (за його суворе розв'язання залежностей у монорепозиторіях).
Рантайм для скриптів та CLI-інструментів — будь-який TypeScript-файл, який мені потрібно запустити один раз, я запускаю з bun. Без кроку компіляції. Швидкий запуск. Вбудоване завантаження .env. Він повністю замінив ts-node і tsx у моєму робочому процесі.
Рантайм для невеликих API та внутрішніх інструментів — Bun.serve() + bun:sqlite — це неймовірно продуктивний стек для внутрішніх інструментів, обробників вебхуків та невеликих сервісів. Модель деплойменту "один бінарник, без залежностей" переконлива.
Тест-раннер для простих проєктів — для проєктів із простими потребами тестування bun test швидкий і не потребує жодної конфігурації.
Де я залишаюся з Node.js#
Продакшн Next.js — не тому, що Bun не працює, а тому, що співвідношення ризику та винагороди ще не виправдовує перехід. Next.js — це складний фреймворк з багатьма точками інтеграції. Я хочу найбільш випробуваний у бою рантайм під ним.
Критично важливі продакшн-сервіси — мої основні API-сервери працюють на Node.js за PM2. Екосистема моніторингу, інструменти відладки, операційні знання — все це Node.js. Bun туди дійде, але ще не дійшов.
Будь-що з нативними аддонами — якщо ланцюжок залежностей включає C++ нативні аддони, я навіть не пробую Bun. Не варто дебажити проблеми сумісності.
Команди, які не знайомі з Bun — представлення Bun як рантайму команді, яка ніколи його не використовувала, додає когнітивне навантаження. Як пакетний менеджер — добре. Як рантайм — зачекайте, поки команда буде готова.
Що я спостерігаю#
Трекер сумісності Bun — коли він досягне 100% для API Node.js, які мене цікавлять, я переоцінюю ситуацію.
Підтримка фреймворків — Next.js, Remix та SvelteKit мають різні рівні підтримки Bun. Коли один із них офіційно підтримуватиме Bun як продакшн-рантайм, це буде сигнал.
Корпоративне впровадження — коли компанії з реальними SLA запускатимуть Bun у продакшені і писатимуть про це, питання зрілості буде вирішене.
Лінійка релізів 1.2+ — Bun рухається швидко. Функції з'являються щотижня. Bun, який я використовую сьогодні, суттєво кращий за Bun, який я спробував рік тому.
Підсумок#
Bun — не срібна куля. Він не зробить повільний застосунок швидким і не зробить погано спроєктований API добре спроєктованим. Але це справжнє покращення досвіду розробника для екосистеми JavaScript.
Те, що я ціную найбільше в Bun — це не якась окрема функція. Це зменшення складності ланцюжка інструментів. Один бінарник, який встановлює пакети, запускає TypeScript, збирає код і запускає тести. Без tsconfig.json для скриптів. Без Babel. Без окремої конфігурації тест-раннера. Просто bun run your-file.ts, і воно працює.
Практична порада: почніть з bun install. Це нульовий ризик, негайна вигода. Потім спробуйте bun run для скриптів. Потім оцініть решту на основі ваших конкретних потреб. Вам не потрібно йти ва-банк. Bun чудово працює як часткова заміна, і саме так більшість людей повинні використовувати його сьогодні.
Ландшафт рантаймів JavaScript стає кращим з Bun у ньому. Конкуренція робить Node.js кращим теж — Node.js 22+ став значно швидшим, частково у відповідь на тиск Bun. Виграють усі.