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 добавляет накладные расходы на middleware. Как только вы добавляете реальную логику — запросы к базе данных, аутентификацию, сериализацию 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 как читаемый запасной вариант. Пояс и подтяжки.
Поддержка Workspace#
Bun поддерживает workspace в стиле npm/yarn:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# Установка зависимостей для всех workspace
bun install
# Запуск скрипта в конкретном workspace
bun run --filter packages/shared build
# Добавление зависимости в конкретный workspace
bun add react --filter apps/webПоддержка workspace стабильная и значительно улучшилась. Основной разрыв по сравнению с pnpm в том, что разрешение зависимостей workspace у 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 — работает, включая экосистему middleware
- 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 # Если файл существует — значит нативныйСпецифичные внутренности 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 test runner
# или
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(`Сервер запущен по адресу http://localhost:${server.port}`);Несколько вещей, на которые стоит обратить внимание:
- Стандартные веб-API Request/Response — никакого проприетарного API. Обработчик
fetchполучает стандартныйRequestи возвращает стандартныйResponse. Если вы писали Cloudflare Worker, это ощущается идентично. Response.json()— встроенный помощник для JSON-ответов.- Не нужен import —
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("Ошибка запроса:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
});
console.log(`Сервер запущен на порту ${server.port}`);Это полноценный CRUD API с SQLite примерно в 50 строках. Без Express, без ORM, без цепочки middleware. Для маленьких 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(`Клиент подключился: ${ws.data.userId}`);
ws.subscribe("chat");
},
message(ws, message) {
// Рассылка всем подписчикам
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`Клиент отключился: ${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(`Вставлено ${count} пользователей`);Это синхронный SQLite с производительностью C-библиотеки (потому что он ей и является — Bun встраивает libsqlite3 напрямую). Для CLI-инструментов, local-first приложений и небольших сервисов встроенный SQLite означает ноль внешних зависимостей для слоя данных.
Тест-раннер Bun#
bun test — это почти полная замена Jest в большинстве случаев. Он использует тот же API describe/it/expect и поддерживает большинство матчеров Jest.
Базовое использование#
// math.test.ts
import { describe, it, expect } from "bun:test";
describe("математические утилиты", () => {
it("корректно складывает числа", () => {
expect(1 + 2).toBe(3);
});
it("обрабатывает числа с плавающей точкой", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
});# Запуск всех тестов
bun test
# Запуск конкретного файла
bun test math.test.ts
# Запуск тестов по паттерну
bun test --test-name-pattern "складывает числа"
# Режим наблюдения
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("возвращает пользователей из базы данных", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Alice");
});
});
// Слежение за методом объекта
describe("console", () => {
it("отслеживает вызовы console.log", () => {
const logSpy = spyOn(console, "log");
console.log("тестовое сообщение");
expect(logSpy).toHaveBeenCalledWith("тестовое сообщение");
logSpy.mockRestore();
});
});Bun Test vs. Vitest — моё честное сравнение#
Для этого проекта (и большинства моих проектов) я использую Vitest. Вот почему я не перешёл полностью:
Где bun test побеждает:
- Скорость запуска.
bun testначинает выполнять тесты быстрее, чем Vitest успевает загрузить конфиг. - Ноль конфигурации. Не нужен
vitest.config.tsдля базовых сетапов. - Встроенный TypeScript. Никакого этапа трансформации.
Где Vitest всё ещё побеждает:
- Экосистема: у Vitest больше плагинов, лучше интеграция с IDE и больше сообщество.
- Конфигурация: система конфигурации Vitest более гибкая. Кастомные репортёры, сложные файлы настройки, несколько тестовых окружений.
- Браузерный режим: Vitest может запускать тесты в реальном браузере. Bun — нет.
- Совместимость: некоторые библиотеки для тестирования (Testing Library, MSW) более тщательно протестированы с Vitest/Jest.
- Снимки (snapshot testing): оба поддерживают, но реализация Vitest более зрелая с лучшим выводом различий.
Для нового проекта с простыми потребностями в тестировании я бы использовал 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
# Генерация sourcemap
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("Сборка не удалась:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.path} — ${output.size} байт`);
}Tree-Shaking#
Bun поддерживает tree-shaking для ESM:
// utils.ts
export function used() {
return "Я попаду в бандл";
}
export function unused() {
return "Меня удалит tree-shaking";
}
// 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(`Собрано ${info.builtAt}, коммит ${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. Hot module replacement работает. API-роуты работают. Серверные компоненты работают.
Что всё ещё экспериментально#
Рантайм Bun для продакшен-сборок Next.js:
# Сборка с рантаймом Bun
bun --bun run build
# Запуск продакшен-сервера с рантаймом Bun
bun --bun run startДля многих проектов это работает, но я встречал граничные случаи:
- Различия в поведении middleware — если вы используете middleware 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 API достигнет 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МБ итого
# vs. 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.tsNode.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 отличается#
// fetch в Node.js 18+ и fetch в Bun немного различаются
// в обработке определённых заголовков и редиректов
// Bun по умолчанию следует редиректам (как браузеры)
// fetch в Node.js тоже следует редиректам, но поведение
// с определёнными кодами статуса (303, 307, 308) может отличаться
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // Будьте явными в обработке редиректов
});2. Поведение при завершении процесса#
// Bun завершается, когда event loop пуст
// Node.js иногда продолжает работать из-за висящих хендлов
// Если ваш скрипт на Bun неожиданно завершается,
// что-то не удерживает event loop активным
// Это немедленно завершится в 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 — не серебряная пуля. Он не сделает медленное приложение быстрым и не превратит плохо спроектированный 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. Выигрывают все.