← все статьи
7 мин

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 и linterdeno 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. Выбирайте рантайм последним, после того как определились с архитектурой, командой и требованиями.

← ПредыдущаяInfrastructure as Code: Terraform vs Pulumi Следующая →Миграции базы данных: как не положить production