К весне 2026 года противостояние между Bun и Node.js перешло из фазы "интересный эксперимент" в практическую плоскость. Bun 1.3 заявляет около 98% совместимости с Node.js API, на нём собрано Claude Code, его рекомендует Vercel для деплоев Next.js. Bun 1.0 вышел в сентябре 2023 и с тех пор накопил два с половиной года правок, релизов и сообщества. Параллельно Node.js 22 LTS догнал Bun по части удобств - встроенный test runner, нативный TypeScript через --experimental-strip-types, стабильная permission model. Вопрос "что выбрать в продакшене" перестал быть теоретическим. Ответ на него зависит уже не от моды и не от бенчмарков, а от конкретного списка пакетов, которые тащит за собой проект. Разбор того, что именно работает, а что ломается, и есть тема этой статьи.

Что Bun делает заметно лучше Node.js и где это реально пригождается

Скорость запуска у Bun действительно радикально ниже, чем у Node.js. Холодный старт обычной JavaScript-программы укладывается в десять миллисекунд против двухсот у Node.js. Для серверов длительной работы разница незаметна, потому что процесс стартует один раз и работает неделями. Но для CLI-инструментов, AI-агентов, скриптов в CI и serverless-функций эти 190 миллисекунд экономии превращаются в ощутимое преимущество.

Менеджер пакетов Bun устанавливает зависимости в несколько раз быстрее npm. На фоне миллиона системных вызовов npm Bun делает около 165 тысяч, использует жёсткие ссылки на глобальный кэш и параллелит работу. На реальных проектах это превращает минуту установки в десять секунд, что в CI-пайплайнах, который гоняется сотни раз в день, выливается в реальные часы экономии и счета за инфраструктуру.

TypeScript работает в Bun из коробки без всякой настройки. Не нужны ts-node, tsx, esbuild-loader, не нужен tsconfig для базовых случаев. Bun запускает .ts-файлы напрямую, понимая весь синтаксис включая enum, namespace и декораторы. Старт TypeScript-сервиса выглядит как одна команда:

bun src/index.ts

Тот же сервис на Node.js 22 требует или предварительной компиляции через tsc, или экспериментального флага --experimental-strip-types, который не поддерживает enum и decorators. Bun здесь честно удобнее, и это видно невооружённым глазом.

HTTP-производительность тоже выше. На простом сервере, отдающем JSON, Bun выдаёт около 110 тысяч запросов в секунду против 60-70 тысяч у Node.js на той же машине. На реальных API-сервисах разница меньше - бутылочное горлышко обычно в базе или внешних вызовах, а не в самом рантайме. Но для прокси-серверов, gateway-сервисов и edge-функций Bun выигрывает заметно.

Где Node.js 22 остаётся прочнее даже при всех плюсах Bun

Главное преимущество Node.js это не скорость, а время в продакшене. Пятнадцать лет история, миллионы пакетов, проверенных в боевых условиях, V8 с зрелым garbage collector, который работает неделями без перезапуска. Для сервисов длительной работы это важнее холодного старта.

Сборщик мусора в Node.js на основе V8 устроен для долгих сценариев. Bun использует JavaScriptCore и Zig с ручным управлением памятью, и на длинных нагрузках периодически возникают сообщения об утечках памяти. Несколько команд в продакшене публично откатывались на Node.js после того, как процессы Bun начинали постепенно разрастаться по памяти. Это не приговор Bun - команда чинит подобные баги по нескольку штук в каждом релизе. Но статус "стабильный долгоиграющий процесс" у Node.js надёжнее.

Совместимость с npm у Node.js полная по определению. Bun даже на отметке 98% означает, что 2% пакетов могут не работать или работать с особенностями. Когда в типичном проекте 800 транзитивных зависимостей, шанс попасть в эти 2% уверенно стремится к 100%. Конкретные случаи, на которые натыкаются регулярно, разбираются ниже.

Экосистема инструментов наблюдаемости вокруг Node.js богаче. APM-агенты от Datadog, New Relic, Sentry имеют первоклассную поддержку Node.js и пока экспериментальную для Bun. Профилировщики, дебаггеры, инструменты для анализа heap-дампов - всё это работает с Node.js без оговорок, для Bun зачастую с ограничениями.

Native-модули как главная точка боли Bun

Самая стабильная категория проблем с Bun связана с native-модулями. Это пакеты, которые внутри содержат скомпилированный C/C++ код, подгружающийся через N-API. Каждая версия Bun компилируется с определённой версией NODE_MODULE_VERSION, и если предкомпилированные бинарники пакета не совпадают по ABI, импорт падает с понятным, но огорчающим сообщением.

Типичный пример с better-sqlite3 выглядит так:

import Database from "better-sqlite3";

const db = new Database("app.db");

При установке через bun add better-sqlite3 на свежей версии Bun импорт может упасть с такой ошибкой:

error: The module 'better_sqlite3' was compiled against a different Node.js
ABI version using NODE_MODULE_VERSION 127. This version of Bun requires
NODE_MODULE_VERSION 137. Please try re-compiling or re-installing the module.

Происходит это потому, что better-sqlite3 при установке сначала пытается скачать готовый prebuilt-бинарник под текущий Node.js ABI, а если не находит подходящего - падает в node-gyp и собирает из исходников. В случае с Bun ситуация осложняется тем, что версия ABI у Bun меняется чаще, чем выходят prebuilds для better-sqlite3. Работает один из обходов:

bun add better-sqlite3 --trust
bun pm trust better-sqlite3

Либо принудительная пересборка через node-gyp, что требует наличия build-инструментов в системе. На macOS это xcode-select --install, на Linux - build-essential, на Windows вообще отдельная сага с Visual Studio Build Tools. На сервере в Docker всё это тоже нужно ставить дополнительно, что увеличивает размер билд-окружения.

Альтернатива для конкретно SQLite - использовать встроенный bun:sqlite, который полностью на стороне Bun и не требует ABI-совместимости:

import { Database } from "bun:sqlite";

const db = new Database("app.db");
const query = db.query("SELECT * FROM users WHERE active = ?");
const users = query.all(1);

Решение работает, но если в проекте используется библиотека вроде Better Auth, которая ждёт именно better-sqlite3-адаптер, переключиться на bun:sqlite не получится. API совместимы не на сто процентов, и Drizzle ORM, Prisma, Better Auth имеют отдельные адаптеры под каждый из них.

Prisma в Bun и почему это отдельная история

Prisma это популярный ORM для Node.js, и его поддержка в Bun исторически была проблемной. Issue-трекеры обеих команд содержат десятки сообщений про "не находит client", "падает на postinstall", "генерация прошла, но импорт сломан".

Базовое использование Prisma в Bun выглядит обычно:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const users = await prisma.user.findMany({
  where: { active: true },
  include: { posts: true },
});

И этот код в большинстве случаев работает, но есть несколько подводных камней. Первый - Prisma до недавнего времени использовала Rust-движок, который запускался как отдельный процесс query-engine. Bun не всегда корректно отрабатывал его сигналы и пайпы, и при больших нагрузках это давало непредсказуемые падения. Команда Prisma выпустила Rust-free engine, который работает целиком в JavaScript - на нём ситуация лучше, но всё ещё не идеальна.

Второй камень касается генерации клиента. Если в schema.prisma указать output для генерируемого кода в нестандартное место, Bun может не найти runtime-файлы. Лечится явным указанием Turbopack/Next.js, что @prisma/client - external package:

// next.config.js
export default {
  experimental: {
    serverExternalPackages: ["@prisma/client", "pg"],
  },
};

Третий - postinstall-скрипты. Prisma при установке запускает скрипт, который скачивает или собирает движок. Bun начиная с версии 1.2 требует явного доверия таким скриптам через bun pm trust, иначе postinstall пропускается, и потом непонятно, почему PrismaClient не находится:

bun add @prisma/client
bun add -d prisma
bun pm trust prisma @prisma/client @prisma/engines
bun prisma generate

Этот шаг легко забывается при первом знакомстве с Bun, и потом часы уходят на отладку странного "Cannot find module '@prisma/client'".

Next.js на Bun, что работает и где аккуратнее не лезть

Vercel в конце 2025 года официально подтвердил поддержку Bun как рантайма для Next.js-приложений. Это серьёзное заявление, потому что Vercel и есть Next.js по сути. Но "поддерживается" не означает "работает идеально для всех случаев". Простые приложения, использующие App Router и стандартные хуки, действительно запускаются под Bun без модификаций:

bun create next-app my-app
cd my-app
bun --bun dev

Флаг --bun здесь критичен. Без него Bun запустит next через установленный node, и преимущества рантайма не сработают. Build-команды настраиваются так же:

{
  "scripts": {
    "dev": "bun --bun next dev",
    "build": "bun --bun next build",
    "start": "bun --bun next start"
  }
}

Проблемы начинаются на edge cases. SSR с тяжёлой middleware-логикой иногда работает медленнее, чем должен, потому что Turbopack оптимизирован под Node.js. ISR (incremental static regeneration) на Bun содержит несколько известных багов с инвалидацией кэша. Custom server (когда поверх Next.js пишется свой Express-обвес) при использовании с Bun иногда странно ведёт себя с асинхронными хуками.

Практический совет такой - для greenfield-проекта на Next.js Bun уже можно использовать. Для миграции большого существующего проекта стоит сначала прогнать все сценарии в стейджинге, особенно тяжёлые страницы с SSR, и только потом переключать прод.

Sharp, canvas, puppeteer и другие тяжеловесы

Sharp - это библиотека обработки изображений с нативным движком libvips. На Node.js она работает безупречно, на Bun до недавнего времени с переменным успехом. В Bun 1.3 ситуация значительно улучшилась, но всё ещё попадаются конфигурации Docker-образов, где Sharp требует ручной пересборки.

Canvas (node-canvas), который тянет за собой Cairo и pixman, имеет похожие проблемы. Если приложение генерирует изображения на сервере (превью, PDF, графики), стоит проверить, что Sharp или Canvas работают в выбранном Docker-образе ещё до того, как полпроекта будет переписано.

Puppeteer и Playwright работают в Bun, но запускают браузеры через подпроцессы, и здесь иногда вылезают расхождения в обработке STDIO. Для большинства сценариев всё нормально, но если используется сложная межпроцессорная коммуникация, могут быть сюрпризы.

Реальная стратегия миграции для существующего проекта

Полная миграция с Node.js на Bun в один шаг это рискованное решение для серьёзного продакшена. Разумнее идти поэтапно:

  1. Заменить npm на bun install в качестве пакетного менеджера. Рантайм остаётся Node.js, но установки зависимостей становятся в разы быстрее. CI экономит минуты;
  2. Перевести dev-окружение на Bun для скриптов и TypeScript. Команды вроде bun run build, bun test заменяют tsx, ts-node, jest. Прод по-прежнему на Node.js;
  3. Прогнать тесты под Bun. Bun.test API совместим с Jest, и большинство тестовых наборов запускаются без модификаций;
  4. Поднять стейдж на Bun. Параллельно с продакшеном на Node.js, неделю-другую посмотреть метрики, профили памяти, латентность;
  5. Только после стабильного периода переключить продакшен. Откат должен быть подготовлен заранее - один флаг в Dockerfile или Helm-чарте.

Каждый этап откатывается независимо. Если на шаге 3 обнаруживается, что Vitest даёт другие результаты по тестам, чем bun test, можно остановиться и не идти дальше до выяснения причин. Если на шаге 4 в логах появляются OOM-killer срабатывания, тоже стоп.

Минимальный Dockerfile для прода под Bun выглядит так:

FROM oven/bun:1.3-alpine AS builder

WORKDIR /app

COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production

COPY . .
RUN bun build --target=bun ./src/index.ts --outdir=./dist


FROM oven/bun:1.3-alpine AS runtime

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000

USER bun

CMD ["bun", "run", "dist/index.js"]

Альтернатива - использовать distroless-образ Google, который требует копирования Bun-бинарника из официального образа. Это уменьшает финальный размер контейнера с 200 МБ до примерно 90 МБ.

Список проектов, для которых выбор очевиден прямо сейчас

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

  • Проект новый и не отягощён существующим Node.js-кодом;
  • Большая часть зависимостей это чистый JavaScript/TypeScript без нативных модулей;
  • Важна скорость холодного старта (CLI, serverless, edge);
  • В CI-пайплайне установка пакетов занимает заметное время;
  • Разработка ведётся на TypeScript и не хочется возиться с tooling.

Выбирать Node.js 22 стоит, когда:

  • Проект уже работает на Node.js, и нет ощутимой проблемы, которую решает миграция;
  • В зависимостях есть пинованные нативные модули (Sharp, better-sqlite3, canvas, специфические ORM-драйверы);
  • Деплой идёт в окружения, поддерживающие только Node.js (AWS Lambda с классическим runtime, некоторые managed-платформы);
  • Команда полагается на устоявшийся APM-стек и инструменты профилирования;
  • Сервис рассчитан на работу неделями без перезапуска под высокой нагрузкой.

Что изменится в ближайшие месяцы

Bun продолжает закрывать совместимостные дыры по нескольку десятков на релиз. К концу 2026 года ожидается, что нативные модули с distutils prebuilds начнут публиковать бинарники под Bun ABI наряду с Node.js. Это снимет львиную долю текущей боли. Node.js параллельно работает над собственным быстрым менеджером пакетов и оптимизациями V8 для холодного старта - то есть гонка идёт в обе стороны.

Главный итог разбора такой. Bun сегодня это не маркетинговая обёртка над Node.js с лучшими бенчмарками, а самостоятельный рантайм со своей нишей. Для серверных API под высокой нагрузкой, для CLI и build-инструментов, для свежих TypeScript-проектов он уже хорош. Для долгоживущих сервисов на сложных стеках с пинованными native-модулями Node.js 22 LTS остаётся более спокойным выбором. Радикально-категоричных решений в этом противостоянии больше нет, есть только конкретный список пакетов и метрик, который определяет правильный ответ для конкретного проекта. И это намного здоровее, чем модный энтузиазм одних и охранительный скептицизм других, потому что инструмент наконец-то выбирается под задачу, а не наоборот.