Перейти к содержимому
·24 мин чтения

Bun в продакшене: что работает, что нет и что удивило

Bun как рантайм, пакетный менеджер, бандлер и тест-раннер. Реальные бенчмарки, пробелы в совместимости с Node.js, паттерны миграции и где я использую Bun в продакшене сегодня.

Поделиться:X / TwitterLinkedIn

Раз в несколько лет в экосистеме JavaScript появляется новый рантайм, и дискуссия развивается по предсказуемой дуге. Хайп. Бенчмарки. «X мёртв». Проверка реальностью. Определение реальных сценариев, где новый инструмент действительно хорош.

Bun сейчас как раз на середине этой дуги. И в отличие от большинства конкурентов, он никуда не уходит. Не потому, что он «быстрее» (хотя зачастую так и есть), а потому, что он решает принципиально другую проблему: в инструментарии JavaScript слишком много движущихся частей, и Bun объединяет их в одно целое.

Я использую Bun в различных форматах уже больше года. Частично в продакшене. Частично — заменяя инструменты, которые думал, что никогда не заменю. Этот пост — честный отчёт о том, что работает, что нет и где пробелы всё ещё имеют значение.

Что такое Bun на самом деле#

Первое заблуждение, которое нужно развеять: Bun — это не «более быстрый Node.js». Такая формулировка его недооценивает.

Bun — это четыре инструмента в одном бинарнике:

  1. Рантайм для JavaScript/TypeScript — выполняет ваш код, как Node.js или Deno
  2. Пакетный менеджер — заменяет npm, yarn или pnpm
  3. Бандлер — заменяет esbuild, webpack или Rollup для определённых сценариев
  4. Тест-раннер — заменяет Jest или Vitest для большинства тестовых наборов

Ключевое архитектурное отличие от Node.js — движок. Node.js использует V8 (движок Chrome). Bun использует JavaScriptCore (движок Safari). Оба зрелые, готовые к продакшену движки, но делают разные компромиссы. JavaScriptCore обычно быстрее запускается и потребляет меньше памяти. V8 обычно показывает более высокую пиковую производительность при длительных вычислениях. На практике эти различия меньше, чем может показаться для большинства нагрузок.

Другой важный отличительный фактор: Bun написан на Zig — системном языке программирования, который находится примерно на том же уровне, что и C, но с лучшими гарантиями безопасности памяти. Именно поэтому Bun может быть таким агрессивным в плане производительности — Zig даёт тот же низкоуровневый контроль, что и C, но без такой плотности ловушек.

bash
# Проверьте версию 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, и разрыв колоссальный.

bash
# Измерение времени запуска — запуск каждого по 100 раз
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Типичные результаты:
# node:  ~40мс
# bun:   ~6мс

Разница примерно в 6-7 раз по времени запуска. Для скриптов, CLI-инструментов и серверлесс-функций, где важен холодный старт, это существенно. Для долгоживущего серверного процесса, который запускается один раз и работает неделями, это не имеет значения.

Установка пакетов#

Это ещё одна область, где Bun уничтожает конкурентов.

bash
# Бенчмарк чистой установки — удалите 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 раз, и она стабильна. Причины в основном:

  1. Бинарный lockfilebun.lockb в бинарном формате, а не JSON. Быстрее читать и записывать.
  2. Глобальный кэш — Bun поддерживает глобальный кэш модулей, так что повторные установки в разных проектах используют уже скачанные пакеты.
  3. I/O на Zig — Сам пакетный менеджер написан на Zig, а не на JavaScript. Файловые операции ввода-вывода ближе к «железу».
  4. Стратегия симлинков — Bun использует жёсткие ссылки из глобального кэша вместо копирования файлов.

Пропускная способность HTTP-сервера#

Встроенный HTTP-сервер Bun быстрый, но сравнения требуют контекста.

bash
# Быстрый и грубый бенчмарк с 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,000

Bun против голого Node.js: примерно 2x для тривиальных ответов. Bun против Express: примерно 7x, но это нечестное сравнение, потому что Express добавляет накладные расходы на middleware. Как только вы добавляете реальную логику — запросы к базе данных, аутентификацию, сериализацию JSON реальных данных — разрыв резко сужается.

Где разница незначительна#

CPU-интенсивные вычисления:

typescript
// 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)}мс`);
bash
bun run fibonacci.ts   # ~1650мс
node fibonacci.ts      # ~1580мс

Node.js (V8) здесь немного побеждает. JIT-компилятор V8 более агрессивен на горячих циклах. Для CPU-интенсивной работы различия между движками примерно одинаковы — иногда V8 выигрывает, иногда JSC, и различия в пределах погрешности.

Как запустить собственные бенчмарки#

Не доверяйте ничьим бенчмаркам, включая мои. Вот как измерить то, что важно для вашей конкретной нагрузки:

bash
# Установите 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. Он просто быстрее, а совместимость отличная.

Основы#

bash
# Установка всех зависимостей из 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 и увидеть, что изменилось. Это важнее, чем хотят признать сторонники скорости.

bash
# Вы можете вывести 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:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Установка зависимостей для всех 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. Переход обычно выглядит так:

bash
# Шаг 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:

typescript
// Всё это работает в 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 вместо него.

bash
# Проверьте, использует ли пакет нативные аддоны
ls node_modules/your-package/binding.gyp  # Если файл существует — значит нативный

Специфичные внутренности Node.js: некоторые пакеты обращаются к внутренним API Node.js, таким как process.binding(), или используют специфичные для V8 API. Они не будут работать в Bun, так как он работает на JavaScriptCore.

typescript
// Это НЕ будет работать в 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 для проекта:

bash
# Запустите встроенную проверку совместимости Bun для вашего проекта
bun --bun node_modules/.bin/your-tool
 
# Флаг --bun форсирует рантайм Bun даже для скриптов из node_modules

Моя рекомендация: не предполагайте совместимость. Прогоните свой набор тестов под Bun перед принятием решения. Это занимает пять минут и экономит часы отладки.

bash
# Быстрая проверка совместимости — прогоните полный набор тестов под Bun
bun test  # Если вы используете bun test runner
# или
bun run vitest  # Если вы используете vitest

Встроенные API Bun#

Здесь Bun становится по-настоящему интересным. Вместо того чтобы просто переимплементировать API Node.js, Bun предоставляет собственные API, разработанные для простоты и скорости.

Bun.serve() — встроенный HTTP-сервер#

Это API, которое я использую чаще всего. Оно чистое, быстрое, и поддержка WebSocket встроена прямо в него.

typescript
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}`);

Несколько вещей, на которые стоит обратить внимание:

  1. Стандартные веб-API Request/Response — никакого проприетарного API. Обработчик fetch получает стандартный Request и возвращает стандартный Response. Если вы писали Cloudflare Worker, это ощущается идентично.
  2. Response.json() — встроенный помощник для JSON-ответов.
  3. Не нужен importBun.serve — глобальный. Никакого require("http").

Вот более реалистичный пример с роутингом, парсингом JSON-тела и обработкой ошибок:

typescript
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():

typescript
// Чтение файлов
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():

typescript
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 прямо в рантайме:

typescript
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.

Базовое использование#

typescript
// 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);
  });
});
bash
# Запуск всех тестов
bun test
 
# Запуск конкретного файла
bun test math.test.ts
 
# Запуск тестов по паттерну
bun test --test-name-pattern "складывает числа"
 
# Режим наблюдения
bun test --watch
 
# Покрытие
bun test --coverage

Мокирование#

Bun поддерживает мокирование, совместимое с Jest:

typescript
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: быстрый, с чёткими мнениями и фокусом на типичных сценариях.

Базовая сборка#

bash
# Сборка одной точки входа
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#

typescript
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:

typescript
// utils.ts
export function used() {
  return "Я попаду в бандл";
}
 
export function unused() {
  return "Меня удалит tree-shaking";
}
 
// index.ts
import { used } from "./utils";
console.log(used());
bash
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#

Одна действительно уникальная фича: выполнение кода во время компиляции через макросы.

typescript
// 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,
  };
}
typescript
// 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 — работает идеально:

bash
# Используйте 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:

bash
# Принудительный запуск Next.js под рантаймом Bun
bun --bun run dev

В большинстве случаев это работает для разработки. Флаг --bun говорит Bun использовать собственный рантайм вместо делегирования Node.js. Hot module replacement работает. API-роуты работают. Серверные компоненты работают.

Что всё ещё экспериментально#

Рантайм Bun для продакшен-сборок Next.js:

bash
# Сборка с рантаймом Bun
bun --bun run build
 
# Запуск продакшен-сервера с рантаймом Bun
bun --bun run start

Для многих проектов это работает, но я встречал граничные случаи:

  1. Различия в поведении middleware — если вы используете middleware Next.js, зависящее от специфичных для Node.js API, вы можете столкнуться с проблемами совместимости.
  2. Оптимизация изображений — пайплайн оптимизации изображений Next.js использует sharp, который является нативным аддоном. Он работает с Bun, но я видел периодические проблемы.
  3. ISR (Incremental Static Regeneration) — работает, но я не нагружал его стресс-тестами под Bun в продакшене.

Моя рекомендация для Next.js#

Используйте Bun как пакетный менеджер. Используйте Node.js как рантайм. Это даёт вам преимущества скорости bun install без какого-либо риска совместимости.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Ежедневный рабочий процесс
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#

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"]

Многоступенчатая сборка для минимального образа#

dockerfile
# Этап сборки: полный образ 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 для деплоя:

bash
# Скомпилируйте приложение в отдельный исполняемый файл
bun build --compile ./src/server.ts --outfile server
 
# Результат — автономный бинарник — для запуска не нужен ни Bun, ни Node.js
./server
dockerfile
# Ультра-минимальный 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. Для контейнерных деплоев автономная природа — значительное упрощение.

Сравнение размеров#

bash
# Образ 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: только пакетный менеджер (нулевой риск)#

bash
# Замените npm/yarn/pnpm на bun install
# Измените ваш CI-пайплайн:
# Было:
npm ci
 
# Стало:
bun install --frozen-lockfile

Никаких изменений кода. Никаких изменений рантайма. Просто более быстрые установки. Если что-то сломается (не сломается), откатитесь, удалив bun.lockb и запустив npm install.

Фаза 2: скрипты и инструменты#

bash
# Используйте 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: тест-раннер (средний риск)#

bash
# Замените vitest/jest на bun test для простых тестовых наборов
bun test
 
# Оставьте vitest для сложных тестовых сетапов
# (Testing Library, MSW, кастомные окружения)

Прогоните полный набор тестов под bun test. Если всё проходит, вы избавились от одной devDependency. Если некоторые тесты падают из-за совместимости, оставьте Vitest для них и используйте bun test для остальных.

Фаза 4: рантайм для новых сервисов (осознанный риск)#

typescript
// Новые микросервисы или API — начинайте с Bun с первого дня
Bun.serve({
  port: 3000,
  fetch(req) {
    // Ваш новый сервис здесь
  },
});

Не мигрируйте существующие сервисы Node.js на рантайм Bun. Вместо этого пишите новые сервисы на Bun с самого начала. Это ограничивает зону поражения.

Фаза 5: миграция рантайма (продвинутая)#

bash
# Только после тщательного тестирования:
# Замените node на bun для существующих сервисов
# Было:
node dist/server.js
 
# Стало:
bun dist/server.js

Я рекомендую это только для сервисов с отличным покрытием тестами. Прогоните нагрузочные тесты под Bun перед переключением продакшена.

Переменные окружения и конфигурация#

Bun обрабатывает .env файлы автоматически — никакого пакета dotenv не нужно:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// Это доступно без каких-либо импортов
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:

bash
# Отладчик Bun — работает с VS Code
bun --inspect run server.ts
 
# bun inspect-brk — пауза на первой строке
bun --inspect-brk run server.ts

Для VS Code добавьте это в .vscode/launch.json:

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-зависимостей. Меньше зависимостей — меньше поверхность для атак через цепочку поставок. Это реальное преимущество безопасности.

bash
# Сравнение количества зависимостей
# Типичный проект Express + SQLite + WebSocket:
npm ls --all | wc -l
# ~340 пакетов
 
# Тот же функционал с встроенными возможностями Bun:
bun pm ls --all | wc -l
# ~12 пакетов (только ваш код приложения)

Это значимое сокращение количества пакетов, которым вы доверяете свою продакшен-нагрузку.

Тюнинг производительности#

Несколько специфичных для Bun советов по производительности:

typescript
// Используйте опции 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");
  },
});
typescript
// Используйте 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;
`);
bash
# Флаги управления памятью Bun
bun --smol run server.ts  # Уменьшить потребление памяти (немного медленнее)
 
# Установить максимальный размер кучи
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512МБ лимит

Типичные ловушки#

За год использования Bun вот что меня подловило:

1. Поведение глобального Fetch отличается#

typescript
// 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. Поведение при завершении процесса#

typescript
// Bun завершается, когда event loop пуст
// Node.js иногда продолжает работать из-за висящих хендлов
 
// Если ваш скрипт на Bun неожиданно завершается,
// что-то не удерживает event loop активным
 
// Это немедленно завершится в Bun:
setTimeout(() => {}, 0);
 
// Это будет работать:
setTimeout(() => {}, 1000);
// (Bun завершится после срабатывания таймаута)

3. Конфигурация TypeScript#

typescript
// У Bun свои дефолтные настройки tsconfig
// Если вы делите проект между Bun и Node.js,
// будьте явными в tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Добавьте определения типов Bun
  }
}
bash
# Установите типы Bun
bun add -d @types/bun

4. Горячая перезагрузка в разработке#

bash
# У Bun есть встроенный режим наблюдения
bun --watch run server.ts
 
# Это перезапускает процесс при изменении файлов
# Это не HMR (Hot Module Replacement) — это полный перезапуск
# Но поскольку Bun запускается очень быстро, ощущается мгновенно

5. Конфигурационный файл bunfig.toml#

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. Выигрывают все.

Похожие записи