Bun как рантайм, пакетный менеджер, бандлер и тест-раннер. Реальные бенчмарки, пробелы в совместимости с Node.js, паттерны миграции и где я использую Bun в продакшене сегодня.
Раз в несколько лет в экосистеме JavaScript появляется новый рантайм, и дискуссия развивается по предсказуемой дуге. Хайп. Бенчмарки. «X мёртв». Проверка реальностью. Определение реальных сценариев, где новый инструмент действительно хорош.
Bun сейчас как раз на середине этой дуги. И в отличие от большинства конкурентов, он никуда не уходит. Не потому, что он «быстрее» (хотя зачастую так и есть), а потому, что он решает принципиально другую проблему: в инструментарии JavaScript слишком много движущихся частей, и Bun объединяет их в одно целое.
Я использую Bun в различных форматах уже больше года. Частично в продакшене. Частично — заменяя инструменты, которые думал, что никогда не заменю. Этот пост — честный отчёт о том, что работает, что нет и где пробелы всё ещё имеют значение.
Первое заблуждение, которое нужно развеять: Bun — это не «более быстрый Node.js». Такая формулировка его недооценивает.
Bun — это четыре инструмента в одном бинарнике:
Ключевое архитектурное отличие от 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 раз, и она стабильна. Причины в основном:
bun.lockb в бинарном формате, а не JSON. Быстрее читать и записывать.Встроенный 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 выигрывает с большим отрывом. Если узкое место — сырые вычисления, результат примерно одинаковый.
Здесь я полностью перешёл. Даже в проектах, где в продакшене работает 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), но ментальная модель идентична.
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 поддерживает 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.
Это раздел, где мне нужно быть особенно аккуратным, потому что ситуация меняется каждый месяц. На начало 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 снимает типы на этапе парсинга.
Фреймворки, которые работают:
Пробелы обычно попадают в несколько категорий:
Нативные аддоны (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Здесь Bun становится по-настоящему интересным. Вместо того чтобы просто переимплементировать API Node.js, Bun предоставляет собственные API, разработанные для простоты и скорости.
Это 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}`);Несколько вещей, на которые стоит обратить внимание:
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("Ошибка запроса:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
});
console.log(`Сервер запущен на порту ${server.port}`);Это полноценный CRUD API с SQLite примерно в 50 строках. Без Express, без ORM, без цепочки middleware. Для маленьких API и внутренних инструментов это теперь мой основной стек.
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 — полноценный гражданин в 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-библиотеки. Для простых фичей реального времени это невероятно удобно.
Это удивило меня больше всего. 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 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 --coverageBun поддерживает мокирование, совместимое с 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();
});
});Для этого проекта (и большинства моих проектов) я использую Vitest. Вот почему я не перешёл полностью:
Где bun test побеждает:
bun test начинает выполнять тесты быстрее, чем Vitest успевает загрузить конфиг.vitest.config.ts для базовых сетапов.Где Vitest всё ещё побеждает:
Для нового проекта с простыми потребностями в тестировании я бы использовал bun test. Для устоявшегося проекта с Testing Library, MSW и сложным мокированием я оставляю Vitest.
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 externalconst result = await Bun.build({
entrypoints: ["./src/index.ts", "./src/worker.ts"],
outdir: "./dist",
target: "browser",
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
splitting: true, // Разделение кода
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} байт`);
}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` не появится в выходном файлеДля сборки библиотеки или простого JS-бандла веб-приложения bun build отлично подходит. Для сложных сборок приложений с CSS-модулями, оптимизацией изображений и кастомными стратегиями чанков вам всё ещё понадобится полноценный бандлер.
Одна действительно уникальная фича: выполнение кода во время компиляции через макросы.
// 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 для установки зависимостей, затем 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Для многих проектов это работает, но я встречал граничные случаи:
Используйте 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 хорошо поддерживается и готов к продакшену.
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, вот инкрементальный путь, который я рекомендую:
# Замените npm/yarn/pnpm на bun install
# Измените ваш CI-пайплайн:
# Было:
npm ci
# Стало:
bun install --frozen-lockfileНикаких изменений кода. Никаких изменений рантайма. Просто более быстрые установки. Если что-то сломается (не сломается), откатитесь, удалив bun.lockb и запустив npm install.
# Используйте 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.
# Замените vitest/jest на bun test для простых тестовых наборов
bun test
# Оставьте vitest для сложных тестовых сетапов
# (Testing Library, MSW, кастомные окружения)Прогоните полный набор тестов под bun test. Если всё проходит, вы избавились от одной devDependency. Если некоторые тесты падают из-за совместимости, оставьте Vitest для них и используйте bun test для остальных.
// Новые микросервисы или API — начинайте с Bun с первого дня
Bun.serve({
port: 3000,
fetch(req) {
// Ваш новый сервис здесь
},
});Не мигрируйте существующие сервисы Node.js на рантайм Bun. Вместо этого пишите новые сервисы на Bun с самого начала. Это ограничивает зону поражения.
# Только после тщательного тестирования:
# Замените 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 вот что меня подловило:
// fetch в Node.js 18+ и fetch в Bun немного различаются
// в обработке определённых заголовков и редиректов
// Bun по умолчанию следует редиректам (как браузеры)
// fetch в Node.js тоже следует редиректам, но поведение
// с определёнными кодами статуса (303, 307, 308) может отличаться
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // Будьте явными в обработке редиректов
});// Bun завершается, когда event loop пуст
// Node.js иногда продолжает работать из-за висящих хендлов
// Если ваш скрипт на Bun неожиданно завершается,
// что-то не удерживает event loop активным
// Это немедленно завершится в Bun:
setTimeout(() => {}, 0);
// Это будет работать:
setTimeout(() => {}, 1000);
// (Bun завершится после срабатывания таймаута)// У Bun свои дефолтные настройки tsconfig
// Если вы делите проект между Bun и Node.js,
// будьте явными в tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Добавьте определения типов Bun
}
}# Установите типы Bun
bun add -d @types/bun# У Bun есть встроенный режим наблюдения
bun --watch run server.ts
# Это перезапускает процесс при изменении файлов
# Это не HMR (Hot Module Replacement) — это полный перезапуск
# Но поскольку Bun запускается очень быстро, ощущается мгновенно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"После года использования в продакшене вот к чему я пришёл:
Пакетный менеджер для всех проектов — включая этот блог на Next.js. bun install быстрее, и совместимость практически идеальная. Я не вижу причин использовать npm или yarn. pnpm — единственная альтернатива, которую я бы рассмотрел (за его строгое разрешение зависимостей в монорепозиториях).
Рантайм для скриптов и CLI-инструментов — любой TypeScript-файл, который мне нужно запустить один раз, я запускаю через bun. Без этапа компиляции. Быстрый запуск. Встроенная загрузка .env. Это полностью заменило ts-node и tsx в моём рабочем процессе.
Рантайм для маленьких API и внутренних инструментов — Bun.serve() + bun:sqlite — это невероятно продуктивный стек для внутренних инструментов, обработчиков вебхуков и небольших сервисов. Модель деплоя «один бинарник, ноль зависимостей» очень привлекательна.
Тест-раннер для простых проектов — для проектов с простыми потребностями в тестировании bun test быстрый и не требует никакой конфигурации.
Продакшен 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. Выигрывают все.