К весне 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 в один шаг это рискованное решение для серьёзного продакшена. Разумнее идти поэтапно:
- Заменить npm на bun install в качестве пакетного менеджера. Рантайм остаётся Node.js, но установки зависимостей становятся в разы быстрее. CI экономит минуты;
- Перевести dev-окружение на Bun для скриптов и TypeScript. Команды вроде bun run build, bun test заменяют tsx, ts-node, jest. Прод по-прежнему на Node.js;
- Прогнать тесты под Bun. Bun.test API совместим с Jest, и большинство тестовых наборов запускаются без модификаций;
- Поднять стейдж на Bun. Параллельно с продакшеном на Node.js, неделю-другую посмотреть метрики, профили памяти, латентность;
- Только после стабильного периода переключить продакшен. Откат должен быть подготовлен заранее - один флаг в 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 остаётся более спокойным выбором. Радикально-категоричных решений в этом противостоянии больше нет, есть только конкретный список пакетов и метрик, который определяет правильный ответ для конкретного проекта. И это намного здоровее, чем модный энтузиазм одних и охранительный скептицизм других, потому что инструмент наконец-то выбирается под задачу, а не наоборот.