Chapter 01

Node.js 与 Bun:运行时对比与选择

深入理解两大运行时的架构差异,从事件循环到引擎内核,找到适合你项目的那一个

什么是 JavaScript 运行时

浏览器之外,JavaScript 需要一个"宿主环境"才能运行——这个环境就是运行时(Runtime)。运行时提供了 JavaScript 引擎之外的一切:文件系统访问、网络 I/O、进程管理、定时器等系统级 API。

Node.js 架构深度解析

Node.js 由 Ryan Dahl 于 2009 年创建,其核心设计思想是:用非阻塞 I/O 和事件驱动模型解决 C10K(同时处理 10000 个连接)问题。

事件循环的六个阶段

Node.js 的事件循环基于 libuv,每次"tick"按顺序经过以下阶段:

┌─────────────────────────────────────────────────────┐
│                    事件循环 (Event Loop)               │
│                                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │  timers  │→ │ pending  │→ │   idle   │          │
│  │setTimeout│  │callbacks │  │ prepare  │          │
│  └──────────┘  └──────────┘  └──────────┘          │
│       ↑                            ↓                │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │  close   │← │  check   │← │   poll   │          │
│  │callbacks │  │setImmed. │  │  I/O等待 │          │
│  └──────────┘  └──────────┘  └──────────┘          │
│                                                     │
│  每个阶段之间:清空 nextTick 队列 和 微任务(Promise)队列   │
└─────────────────────────────────────────────────────┘
// 执行顺序示例
console.log('1 同步代码');

setTimeout(() => console.log('4 timers 阶段'), 0);

Promise.resolve().then(() => console.log('3 微任务队列'));

process.nextTick(() => console.log('2 nextTick (优先级最高)'));

// 输出顺序: 1 → 2 → 3 → 4

process.nextTick vs Promise.then:两者都在当前操作完成后、下一事件循环阶段前执行(微任务),但 nextTick 的优先级更高。滥用 nextTick 可能会导致 I/O 饿死——如果 nextTick 不断递归调用自身,poll 阶段永远无法执行。

libuv 线程池

虽然 Node.js 的 JavaScript 是单线程的,但 libuv 维护了一个默认大小为 4 的线程池,用于处理:

# 可通过环境变量调整线程池大小(最大 1024)
UV_THREADPOOL_SIZE=16 node server.js

Bun 的架构创新

Bun 由 Jarred Sumner 于 2021 年开始开发,2023 年发布 1.0 正式版。它不只是一个运行时,更是一个完整的 JavaScript 工具链

Bun 包含
  • JavaScript 运行时(JSC 引擎)
  • 包管理器(替代 npm/yarn/pnpm)
  • 打包器(替代 webpack/esbuild)
  • 测试运行器(替代 Jest/Vitest)
  • TypeScript/JSX 原生转译
Node.js 需要
  • 运行时(V8 引擎)
  • npm / pnpm / yarn(独立安装)
  • webpack / esbuild(独立安装)
  • Jest / Vitest(独立安装)
  • ts-node / tsx / esbuild(独立)

为什么选 JavaScriptCore 而非 V8

这是 Bun 最具争议的设计决策之一。JSC 相比 V8 的优势:

指标V8(Node.js)JSC(Bun)
冷启动时间较慢(更多预热优化)更快(低延迟优先)
内存占用较高更低
长期峰值性能极高(JIT 充分预热后)略低于 V8 峰值
适合场景长时间运行的服务短命令/边缘计算/开发工具

性能对比基准数据

以下数据来自 Bun 官方基准测试(2024 年,Apple M1 Pro),实际生产环境因业务逻辑不同会有差异:

场景Node.js 22Bun 1.x提升
HTTP Hello World(req/s)~65,000~120,000+84%
bun install(冷缓存)npm ~10s~1.5s6.7x
bun install(热缓存)npm ~5s~180ms27x
脚本启动(--version~70ms~5ms14x
SQLite 读取(100万行)better-sqlite3 ~0.8s内置 ~0.3s2.7x

基准测试的局限性:以上数据是"Hello World"级别的压测。真实业务中,数据库查询、复杂业务逻辑、外部 API 调用等才是性能瓶颈。对于大多数 CRUD 应用,两者性能差异完全感知不到。不要因为 Bun 快就抛弃 Node.js 的成熟生态。

TypeScript 原生支持

这是 Bun 最受开发者喜爱的特性之一——无需任何配置,直接执行 .ts 文件:

Node.js — 需要配置
# 方案1:ts-node
npm install -D ts-node typescript
npx ts-node server.ts

# 方案2:tsx(推荐)
npm install -D tsx
npx tsx server.ts

# 方案3:Node.js 22.6+ 实验性支持
node --experimental-strip-types server.ts
Bun — 开箱即用
# 直接运行,无需任何安装
bun run server.ts

# 也支持 JSX/TSX
bun run app.tsx

# 监视模式(类似 nodemon)
bun --watch run server.ts

Bun 的 TS 转译是"类型擦除":Bun 并不做类型检查,只是去掉类型注解执行。这意味着类型错误在运行时不会被发现——你仍然需要在 CI 流程中运行 tsc --noEmit 做类型检查。

Node.js 22+ 新特性

Node.js 并没有停滞不前。22.x LTS(2024 年 10 月起)带来了一系列重要更新:

// Node.js 22 内置测试(node:test)
import { test, describe } from 'node:test';
import assert from 'node:assert';

test('两数相加', () => {
  assert.strictEqual(1 + 1, 2);
});

安装与工具链配置

安装 Node.js(推荐使用版本管理器)

# 方案1:fnm(Fast Node Manager,Rust 编写,推荐)
curl -fsSL https://fnm.vercel.app/install | bash
fnm install 22
fnm use 22

# 方案2:nvm(Node Version Manager,最广泛)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 22
nvm use 22

# 验证
node --version  # v22.x.x
npm --version   # 10.x.x

安装 Bun

# macOS / Linux
curl -fsSL https://bun.sh/install | bash

# Windows(PowerShell)
irm bun.sh/install.ps1 | iex

# 通过 npm 安装(适合 CI 环境)
npm install -g bun

# 升级 Bun
bun upgrade

# 验证
bun --version  # 1.x.x

创建第一个 Bun 项目

# 初始化项目
mkdir my-backend && cd my-backend
bun init

# bun init 会生成:
# ├── index.ts       — 入口文件
# ├── package.json   — 项目配置
# ├── tsconfig.json  — TypeScript 配置
# └── .gitignore

# 运行
bun run index.ts

何时选择 Bun,何时选 Node.js

场景推荐理由
全新项目,追求开发体验Bun原生 TS、极速安装、开箱即用
成熟产品,稳定性优先Node.jsLTS 支持、生态最成熟
Cloudflare Workers / 边缘Bun / Hono低冷启动,Hono 专为边缘设计
企业级、合规要求Node.js经过大量生产验证
脚本工具 / CLIBun启动速度快,Shell 内置
依赖 native addon(.node 文件)Node.jsBun 的 Node-API 兼容性仍在完善

实用策略:用 bun 作为包管理器和开发运行器(bun installbun run dev),用 node 作为生产运行器。这样既享受 Bun 的开发速度,又保留 Node.js 的生产稳定性。很多项目正在采用这种混合策略。

Node.js 22 LTS 核心新特性

Node.js 22(2024年发布,LTS 至 2027年)带来了多项重要更新:

原生 TypeScript 支持(--experimental-strip-types
Node.js 22.6+ 实验性支持直接运行 TypeScript 文件,使用 Amaro(基于 swc)去掉类型注解后执行。注意:不进行类型检查,仅剥离类型语法。Node.js 23 将该功能设为默认启用。node --experimental-strip-types app.ts
内置 WebSocket 客户端
Node.js 22 将 WebSocket 客户端(基于 WHATWG WebSocket API)升级为稳定特性,无需安装 ws 库:const ws = new WebSocket('wss://example.com')
require() 支持 ES 模块
Node.js 22 实验性支持 require() 同步加载 ES Module(无 top-level await 的模块)。这解决了历史上 CJS/ESM 互操作的痛点,标志着两种模块系统正在融合。
Corepack 包管理器管理
Corepack 是 Node.js 内置的包管理器版本管理工具,通过 package.json 的 "packageManager": "bun@1.1.0" 字段声明项目使用的包管理器和版本,团队成员会自动使用相同版本,避免版本不一致问题。
# Node.js 22: 直接运行 TypeScript(无需编译)
node --experimental-strip-types app.ts

# Node.js 23+: 默认支持(无需 --experimental 标志)
node app.ts

# 内置 WebSocket 客户端(Node.js 22)
// 内置 WebSocket 客户端(无需 ws 库)
const ws = new WebSocket('wss://echo.websocket.org');

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'ping' }));
});

ws.addEventListener('message', (event) => {
  console.log('收到:', event.data);
  ws.close();
});
本章小结

Node.js 和 Bun 代表了 JavaScript 服务端运行时的两个发展方向:
Node.js 22 LTS:成熟稳定,生态最广,原生 TypeScript 支持(--experimental-strip-types)、内置 WebSocket、require() 支持 ESM 等新特性大幅改善了开发体验。
Bun 1.x:以 JavaScriptCore 引擎为基础,追求极致性能(启动速度 3× Node.js,包安装 25× npm)。原生 TS/JSX 支持、内置 SQLite、S3/Redis 客户端使其成为开发生产力利器。
ESM vs CJS:现代项目推荐使用 ESM("type": "module"),可以使用 import/export、top-level await。CJS 的 require() 机制仍被大量遗留代码使用,Node.js 22 正在弥合两者的互操作差距。
选型建议:新项目用 Bun 开发,Node.js 22 LTS 生产;企业合规项目直接用 Node.js LTS 全程。