Node.js, Deno, Bun: выбор рантайма в 2025
Война рантаймов JavaScript идёт с 2018 года, когда Райан Даль (создатель Node.js) вышел на JSConf и перечислил свои сожаления о Node. В 2025 году у нас есть три полноценных конкурента: Node.js 22 LTS, Deno 2.0 и Bun 1.1. Каждый из них решает реальные проблемы. Но так ли сильно они отличаются на практике?
Спойлер: для большинства production-нагрузок разница в производительности незначительна. Выбор рантайма определяется экосистемой, опытом команды и конкретными требованиями проекта — не синтетическими бенчмарками.
Node.js 22 LTS: зрелость с накопленным долгом
Node.js 22 стал LTS в октябре 2024 года. Версия принесла несколько давно ожидаемых изменений.
require(esm) без флага — исторически Node требовал либо .mjs расширение, либо "type": "module" в package.json для ESM. Node 22 наконец поддерживает require() для синхронной загрузки ES-модулей без --experimental-require-module:
// Node 22: require() для ESM-пакетов работает нативно
// Раньше это ломало CJS-проекты при попытке использовать ESM-only библиотеки
// CJS-файл (index.cjs):
const { default: chalk } = require("chalk"); // chalk 5.x — ESM-only
console.log(chalk.green("It works!"));
// Node 22 больше не бросает ERR_REQUIRE_ESM для синхронных ESM
// Важно: работает только для "synchronous top-level modules"
// Динамический import() всё равно предпочтительнее для async
WebSocket client — встроенный, без ws или socket.io для простых случаев:
// Node 22: встроенный WebSocket (без npm install ws)
// Работает в браузерном API-стиле
const ws = new WebSocket("wss://echo.websocket.org");
ws.addEventListener("open", () => {
console.log("Connected");
ws.send(JSON.stringify({ type: "ping" }));
});
ws.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
ws.close();
});
ws.addEventListener("error", (err) => {
console.error("WebSocket error:", err);
});
Watch mode стабилен — node --watch server.js теперь без --experimental. Для многих проектов nodemon больше не нужен:
# Node 22: стабильный watch mode
node --watch server.js
node --watch --watch-path=./src server.js # следим за конкретной директорией
# В package.json:
# "scripts": { "dev": "node --watch src/index.js" }
Test runner — встроенный node:test стал значительно лучше: поддержка --test-reporter, фильтрация по паттерну, coverage из коробки:
// Node 22: встроенный test runner
import { test, describe, before } from "node:test";
import assert from "node:assert/strict";
// node --test — запустить все *.test.js
// node --test --test-name-pattern="payment"
// node --test --experimental-test-coverage
describe("Payment service", () => {
before(async () => {
// setup
});
test("should charge correctly", async (t) => {
await t.test("with valid card", async () => {
const result = await charge("tok_visa", 100);
assert.equal(result.status, "success");
assert.equal(result.amount, 100);
});
await t.test("with declined card", async () => {
await assert.rejects(
() => charge("tok_chargeDeclined", 100),
{ code: "CARD_DECLINED" },
);
});
});
});
Deno 2.0: совместимость с npm и взрослая экосистема
Deno 2.0 вышел в октябре 2024 и сделал самое важное: полноценную совместимость с npm. Первая версия Deno была принципиально против npm, что делало её нишевым инструментом. Deno 2 признал реальность.
// Deno 2.0: импорт из npm без установки
// npm: префикс работает нативно
import express from "npm:express@4";
import { z } from "npm:zod@3";
import chalk from "npm:chalk@5";
// Или через deno.json (как package.json):
// {
// "imports": {
// "express": "npm:express@4",
// "zod": "npm:zod@3"
// }
// }
// Затем в коде — стандартный импорт:
import express from "express";
import { z } from "zod";
deno.json vs package.json — Deno 2 поддерживает оба. Если в директории есть package.json, Deno ведёт себя как Node-совместимый рантайм:
// deno.json — конфиг Deno-проекта
// {
// "tasks": {
// "dev": "deno run --watch --allow-net --allow-read src/main.ts",
// "test": "deno test --allow-net",
// "fmt": "deno fmt",
// "lint": "deno lint"
// },
// "imports": {
// "@std/http": "jsr:@std/http@^1.0",
// "zod": "npm:zod@^3.22"
// },
// "compilerOptions": {
// "strict": true
// }
// }
// Deno 2: встроенный HTTP-сервер (стабильный)
import { serve } from "jsr:@std/http@1";
await serve(
(req: Request): Response => {
const url = new URL(req.url);
if (url.pathname === "/health") {
return Response.json({ status: "ok" });
}
return new Response("Not Found", { status: 404 });
},
{ port: 8000, onListen: ({ port }) => console.log(`Listening on :${port}`) },
);
JSR (JavaScript Registry) — новый реестр пакетов от Deno, совместимый и с Node, и с Deno. Фокус на TypeScript-first пакетах с автодокументацией и встроенной проверкой типов:
# JSR: работает из Node тоже
npx jsr add @std/http # для Node-проектов
deno add jsr:@std/http # для Deno-проектов
# JSR-пакеты:
# jsr:@std/http — HTTP utilities
# jsr:@std/assert — assertions
# jsr:@std/fmt — formatting
# jsr:@hono/hono — Hono web framework
# jsr:@oak/oak — Oak web framework
Встроенный formatter и linter — deno fmt и deno lint без конфигурации. Для нового проекта это существенная экономия времени на настройку toolchain:
# Deno: встроенный toolchain — ничего устанавливать не надо
deno fmt # форматирует весь проект (аналог Prettier)
deno fmt --check # проверка без изменений (для CI)
deno lint # линтинг
deno lint --rules-exclude=no-explicit-any
deno compile src/main.ts # компиляция в standalone binary
deno bundle src/main.ts # бандлинг (deprecated в 2.x, используйте esbuild)
# Permissions (security model):
deno run --allow-net=api.example.com --allow-read=./data src/main.ts
# Без флагов — полный sandbox, никаких IO
Bun 1.1: скорость, Windows, встроенный SQLite
Bun 1.1 вышел в апреле 2024. Zig-based рантайм на JavaScriptCore (движок Safari) продолжает удивлять скоростью запуска и установки пакетов.
Windows support — Bun 1.1 принёс нативную поддержку Windows без WSL. Для Windows-разработчиков это был блокер.
# Bun 1.1: установка зависимостей быстрее npm в 25x
bun install # vs npm install
bun add express zod # vs npm install
bun add -d @types/node # dev dependency
# Bun как drop-in замена Node:
bun run server.js # запуск Node-файла в Bun
bun run start # запуск npm-скрипта
bun test # встроенный test runner (Jest-совместимый API)
bun:sqlite — встроенный SQLite без нативных аддонов. Быстрее better-sqlite3 и не требует компиляции:
// Bun: встроенный SQLite (только в Bun, не portable)
import { Database } from "bun:sqlite";
const db = new Database("myapp.db", { create: true });
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Prepared statements — быстро и безопасно
const insert = db.prepare("INSERT INTO users (email) VALUES (?) RETURNING id");
const { id } = insert.get("user@example.com") as { id: number };
console.log("Created user:", id);
// Query
const getUser = db.prepare("SELECT * FROM users WHERE id = ?");
const user = getUser.get(id);
console.log(user);
db.close();
Macro system — компиляция кода во время бандлинга. Функции с суффиксом .macro выполняются при build и их результат инлайнится в бандл:
// getVersion.macro.ts
export function getVersion() {
// Выполняется во время bun build, не в runtime
return process.env.npm_package_version ?? "unknown";
}
// app.ts — использование макроса
import { getVersion } from "./getVersion.macro";
// После bun build:
// console.log("1.2.3") — строка инлайнится
console.log(getVersion());
// Полезно для: env-переменных в static, codegen, feature detection
Производительность: честное сравнение
Синтетические бенчмарки показывают Bun в 2–4x быстрее Node для HTTP-сервера. В реальных условиях разница значительно меньше. Вот почему:
- Реальные нагрузки ограничены IO, а не CPU-производительностью рантайма. Если сервер ждёт PostgreSQL 20ms, разница между Node и Bun в 0.5ms на HTTP-overhead — это 2.5%.
- Middleware и ORM добавляют накладные расходы, которые нивелируют разницу рантаймов. Express + Prisma в Bun vs Node — почти одинаково.
- Startup time реально важен для serverless. Cold start Lambda с Node: ~300ms. С Bun: ~80ms. Разница 220ms ощутима в serverless-контексте.
# Ориентировочные числа (результаты варьируются по конфигурации)
# HTTP echo server, 1000 concurrent connections, локальная машина:
# Bun (Hono): ~120,000 req/s
# Deno (std/http): ~95,000 req/s
# Node (Fastify): ~80,000 req/s
# Node (Express): ~45,000 req/s
# File I/O (read 1MB file x 10,000):
# Bun: ~1.8s
# Node: ~2.1s
# Deno: ~2.3s
# npm install (Express + TypeScript):
# Bun: ~0.8s
# npm: ~18s
# pnpm: ~4s
# Startup time (print hello world):
# Bun: ~7ms
# Deno: ~45ms
# Node: ~65ms
# Важно: в реальных backend-приложениях с БД и внешними API
# разница между рантаймами — 5-15%, не 200%
Экосистема: npm-совместимость, TypeScript, toolchain
Экосистема npm — главный актив Node.js. Более 2 миллионов пакетов, годы проверки в production.
// Совместимость пакетов npm (примерная оценка, середина 2025):
//
// Node 22: 100% — это npm
// Bun 1.1: ~97% пакетов работают (проблемы с нативными аддонами)
// Deno 2.0: ~95% через npm: prefix (нативные аддоны — проблема)
//
// Нативные аддоны (C++/N-API):
// - sharp (обработка изображений) — проблемы в Bun/Deno
// - canvas — проблемы
// - bcrypt (нативная версия) — проблемы
// Решение: использовать pure-JS альтернативы или компилировать
// (Bun поддерживает N-API, но не все аддоны протестированы)
// TypeScript:
// Node 22: требует ts-node / tsx / esbuild (нет нативной поддержки)
// Bun 1.1: нативно, без transpile step
// Deno 2.0: нативно, без transpile step
// Node 22.6+ --experimental-strip-types (в 2024):
// node --experimental-strip-types server.ts
// Убирает типы, но не трансформирует enum/decorator
# Node: стандартная настройка TypeScript (2025)
# package.json
# "scripts": {
# "dev": "tsx watch src/index.ts",
# "build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
# "start": "node dist/index.js"
# }
# Bun: TypeScript без настройки
bun run src/index.ts # просто работает
bun build src/index.ts --outdir dist --target node
# Deno: TypeScript без настройки
deno run src/index.ts # просто работает
deno compile src/index.ts # standalone binary
Миграция: Node → Deno, Node → Bun
Node → Bun — самая простая миграция. Bun позиционирует себя как drop-in замену:
# Миграция Node → Bun (типичный проект)
# 1. Установка Bun
curl -fsSL https://bun.sh/install | bash
# 2. Замена node_modules (обычно просто переустановка)
rm -rf node_modules
bun install # читает существующий package.json
# 3. Замена скриптов запуска
# package.json: "start": "node dist/index.js" → "start": "bun dist/index.js"
# package.json: "dev": "nodemon src/index.ts" → "dev": "bun --watch src/index.ts"
# 4. Проблемы, которые нужно проверить:
# - Нативные аддоны: sharp, canvas, node-gyp зависимости
# - __dirname / __filename — работают в Bun (в отличие от ESM Node)
# - process.env — работает
# - Node built-ins (fs, path, http) — работают
# 5. Bun-specific оптимизации (опционально):
# - bun:sqlite вместо better-sqlite3
# - Bun.file() вместо fs.readFile
# - Bun.serve() вместо http.createServer()
# Миграция Node → Deno (сложнее)
# 1. Deno 2 поддерживает package.json — начинаем с этого
# В директории с package.json: deno run src/index.ts
# 2. Permissions — нужно добавить флаги:
# deno run --allow-net --allow-read --allow-env src/index.ts
# Или используйте --allow-all для начала (потом ужесточьте)
# 3. Замена Node built-ins:
# import fs from "node:fs" → работает в Deno 2
# import path from "node:path" → работает
# import crypto from "node:crypto" → работает
# import cluster from "node:cluster" → не поддерживается
# 4. Что точно не работает:
# - node-gyp нативные аддоны
# - __dirname (используйте import.meta.dirname)
# - require() в ESM (используйте import)
# 5. deno.json tasks для замены npm-скриптов:
# "tasks": {
# "dev": "deno run --watch --allow-net --allow-env src/index.ts",
# "test": "deno test --allow-net",
# "lint": "deno lint && deno fmt --check"
# }
Фреймворки и экосистема в 2025
// Hono — универсальный фреймворк для всех рантаймов
// Один и тот же код работает на Node, Deno, Bun, Cloudflare Workers
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono();
app.use("*", cors());
app.use("*", logger());
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
app.post("/users", zValidator("json", userSchema), async (c) => {
const { email, name } = c.req.valid("json");
// ...
return c.json({ id: 1, email, name }, 201);
});
app.get("/health", (c) => c.json({ status: "ok" }));
// Запуск:
// Bun: export default app; (Bun.serve через hono/bun)
// Deno: Deno.serve(app.fetch);
// Node: serve(app, { port: 3000 }); // from @hono/node-server
Фреймворк принятия решения
Честная матрица для выбора рантайма в 2025:
Выбирайте Node.js 22 LTS если:
- Enterprise-проект с требованием LTS-поддержки и долгосрочной стабильности
- Активное использование нативных аддонов (sharp, canvas, node-gyp)
- Большая команда с устоявшимися Node-практиками
- Существующая кодовая база, которую нет смысла мигрировать
- Фреймворки, не поддерживающие другие рантаймы (NestJS в полной мере)
Выбирайте Deno 2.0 если:
- Безопасность — критический требование (permissions model из коробки)
- Greenfield TypeScript-проект без нативных аддонов
- Нужен нулевой toolchain setup (fmt, lint, test встроены)
- Команда ценит чистоту и современные веб-стандарты (Web API first)
- Deploy на Deno Deploy или аналогичные edge-платформы
Выбирайте Bun 1.1 если:
- Serverless / edge с холодным стартом — каждая миллисекунда важна
- Скорость установки зависимостей критична (большие монорепо, CI)
- CLI-инструменты и скрипты, где startup time ощутим
- Greenfield проект, где команда открыта к новым технологиям
- SQLite как основная БД (bun:sqlite быстр и прост)
// Итоговая рекомендация по типу проекта:
const decision = {
// Serverless (AWS Lambda, Cloudflare Workers)
serverless: "Bun (startup time) или Deno (edge natives)",
// Традиционный REST API / GraphQL backend
api: "Node LTS (стабильность) или Bun (если нет нативных аддонов)",
// CLI-инструменты
cli: "Bun (startup + compile) или Deno (compile + permissions)",
// Скрипты и автоматизация
scripts: "Bun (быстро, TypeScript нативно)",
// Enterprise / финтех / healthcare
enterprise: "Node LTS — без вопросов",
// Greenfield команда из TypeScript-разработчиков
greenfield: "Deno или Bun — зависит от требований к экосистеме",
};
В 2025 году война рантаймов стала менее войной и больше — конкуренцией идей. Deno подтолкнул Node к нативному TypeScript. Bun показал, что установка пакетов может быть мгновенной. Node взял лучшее и реализовал в 22 LTS. В результате все три рантайма стали лучше.
Не переходите ради перехода. Если ваш Node-проект работает — он будет работать ещё долго. Но если начинаете новый проект и хотите попробовать: Bun для максимальной совместимости с Node при лучшем DX, Deno для чистого TypeScript-проекта с безопасностью из коробки.
Рантайм — не архитектура. Плохо написанный код на Bun медленнее хорошего кода на Node. Выбирайте рантайм последним, после того как определились с архитектурой, командой и требованиями.