跳至内容
·12 分钟阅读

Bun 实战:什么好使、什么不行、什么出乎意料

Bun 作为运行时、包管理器、打包器和测试运行器的真实使用体验。真实基准测试、Node.js 兼容性缺口、迁移模式,以及我今天在生产中使用 Bun 的场景。

分享:X / TwitterLinkedIn

每隔几年,JavaScript 生态系统就会出现一个新运行时,随之而来的讨论总是遵循着可预见的轨迹。炒作。基准测试。"X 已死。"现实检验。最终沉淀到新工具真正擅长的实际用例。

Bun 正处于这条轨迹的中间阶段。与大多数挑战者不同,它留了下来。不是因为它"更快"(虽然它确实经常更快),而是因为它在解决一个真正不同的问题:JavaScript 工具链有太多活动部件,Bun 把它们合并成了一个。

我已经在各种场景下使用 Bun 一年多了。一部分是在生产环境。一部分是替换了我以为永远不会替换的工具。这篇文章是对什么好用、什么不好用、以及差距仍然重要的领域的诚实记录。

Bun 到底是什么#

首先需要纠正一个误解:Bun 不是"更快的 Node.js"。这种说法低估了它。

Bun 是四个工具合一的单一二进制文件:

  1. JavaScript/TypeScript 运行时 — 运行你的代码,类似 Node.js 或 Deno
  2. 包管理器 — 替代 npm、yarn 或 pnpm
  3. 打包器 — 在某些场景下替代 esbuild、webpack 或 Rollup
  4. 测试运行器 — 替代大多数测试套件中的 Jest 或 Vitest

与 Node.js 的关键架构差异在于引擎。Node.js 使用 V8(Chrome 的引擎)。Bun 使用 JavaScriptCore(Safari 的引擎)。两者都是成熟的、生产级的引擎,但它们做出了不同的权衡。JavaScriptCore 往往有更快的启动时间和更低的内存开销。V8 往往在长时间运行的计算中有更好的峰值吞吐量。实际上,对于大多数工作负载,这些差异比你想象的要小。

另一个主要差异点:Bun 是用 Zig 编写的,一种系统编程语言,大致处于与 C 相同的层级,但具有更好的内存安全保障。这就是 Bun 能够在性能上如此激进的原因——Zig 提供了 C 所给予的那种底层控制能力,但没有 C 那样密集的陷阱。

bash
# 检查你的 Bun 版本
bun --version
 
# 直接运行 TypeScript 文件——无需 tsconfig,无需编译步骤
bun run server.ts
 
# 安装包
bun install
 
# 运行测试
bun test
 
# 生产环境打包
bun build ./src/index.ts --outdir ./dist

这是一个二进制文件完成了 node + npm + esbuild + vitest 的工作。无论你喜欢还是讨厌,这确实是一个令人信服的复杂度缩减。

速度声明——诚实的基准测试#

让我直说:Bun 的营销基准测试是精挑细选的。不是造假——是精挑细选。它们展示的是 Bun 表现最好的场景,这正是你对营销材料的预期。问题在于人们从这些基准测试推断出 Bun 在所有方面都"快 25 倍",但事实绝非如此。

以下是 Bun 真正、明显更快的地方:

启动时间#

这是 Bun 最大的真实优势,而且差距明显。

bash
# 测量启动时间——各运行 100 次
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# 典型结果:
# node:  ~40ms
# bun:   ~6ms

大约是 6-7 倍的启动时间差异。对于脚本、CLI 工具以及冷启动很重要的 Serverless 函数来说,这很显著。对于启动一次然后运行数周的长期运行服务器进程来说,这无关紧要。

包安装#

这是 Bun 碾压竞争对手的另一个领域。

bash
# 全新安装基准测试——先删除 node_modules 和 lockfile
rm -rf node_modules bun.lockb package-lock.json
 
# 计时 npm
time npm install
# Real: ~18.4s(典型中等大小项目)
 
# 计时 bun
time bun install
# Real: ~2.1s

这是 8-9 倍的差异,而且非常稳定。原因主要是:

  1. 二进制 lockfilebun.lockb 是二进制格式,不是 JSON。读写更快。
  2. 全局缓存 — Bun 维护全局模块缓存,跨项目的重复安装共享已下载的包。
  3. Zig 的 I/O — 包管理器本身是用 Zig 编写的,不是 JavaScript。文件 I/O 操作更接近底层。
  4. 符号链接策略 — Bun 使用硬链接到全局缓存,而不是复制文件。

HTTP 服务器吞吐量#

Bun 内置的 HTTP 服务器很快,但比较需要上下文。

bash
# 用 bombardier 做快速粗糙的基准测试
# 测试简单的 "Hello World" 响应
 
# Bun 服务器
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
 
# Node.js(原生 http 模块)
bombardier -c 100 -d 10s http://localhost:3001
# Requests/sec: ~48,000
 
# Node.js(Express)
bombardier -c 100 -d 10s http://localhost:3002
# Requests/sec: ~15,000

Bun vs. 原生 Node.js:对于简单响应大约快 2 倍。Bun vs. Express:大约快 7 倍,但这不公平,因为 Express 增加了中间件开销。一旦你加入真实逻辑——数据库查询、认证、实际数据的 JSON 序列化——差距就急剧缩小了。

差异可忽略的场景#

CPU 密集型计算:

typescript
// fibonacci.ts——这是受引擎限制的,不是受运行时限制的
function fib(n: number): number {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
 
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);
bash
bun run fibonacci.ts   # ~1650ms
node fibonacci.ts      # ~1580ms

Node.js(V8)在这里实际上略胜一筹。V8 的 JIT 编译器在热循环上更加激进。对于 CPU 密集型工作,引擎差异基本打平——有时 V8 赢,有时 JSC 赢,差异在噪声范围内。

如何运行自己的基准测试#

不要相信任何人的基准测试,包括我的。以下是如何测量对你具体工作负载真正重要的指标:

bash
# 安装 hyperfine 进行正式基准测试
brew install hyperfine  # macOS
# 或者: cargo install hyperfine
 
# 基准测试你实际应用的启动+执行
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# 对于 HTTP 服务器,使用 bombardier 或 wrk
# 重要:使用真实的 payload 测试,而不是 "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# 内存比较
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

经验法则:如果你的瓶颈是 I/O(文件系统、网络、数据库),Bun 的优势不大。如果你的瓶颈是启动时间或工具链速度,Bun 大胜。如果你的瓶颈是原始计算,那就势均力敌。

Bun 作为包管理器#

这是我已经完全切换的领域。即使在生产环境运行 Node.js 的项目上,我也在本地开发和 CI 中使用 bun install。它就是更快,而且兼容性非常好。

基础用法#

bash
# 从 package.json 安装所有依赖
bun install
 
# 添加依赖
bun add express
 
# 添加开发依赖
bun add -d vitest
 
# 删除依赖
bun remove express
 
# 更新依赖
bun update express
 
# 安装特定版本
bun add express@4.18.2

如果你用过 npm 或 yarn,这一切都很熟悉。标志位略有不同(-d 而不是 --save-dev),但心智模型完全一样。

Lockfile 的情况#

Bun 使用 bun.lockb,一种二进制 lockfile。这既是它的超级武器,也是最大的摩擦点。

优点:读写速度显著更快。二进制格式意味着 Bun 可以在微秒内解析 lockfile,而不是 npm 解析 package-lock.json 所花费的数百毫秒。

缺点:你无法在 diff 中审查它。如果你在团队中工作,有人更新了依赖,你无法在 PR 中查看 lockfile 的 diff 来了解发生了什么变化。这比速度倡导者愿意承认的更重要。

bash
# 你可以将 lockfile 转储为人类可读格式
bun bun.lockb > lockfile-dump.txt
 
# 或者使用内置的文本输出
bun install --yarn
# 这会在 bun.lockb 旁边生成一个 yarn.lock

我的做法:将 bun.lockb 提交到仓库,同时生成一个 yarn.lockpackage-lock.json 作为可读备份。双重保险。

Workspace 支持#

Bun 支持 npm/yarn 风格的 workspace:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# 安装所有 workspace 的依赖
bun install
 
# 在特定 workspace 中运行脚本
bun run --filter packages/shared build
 
# 向特定 workspace 添加依赖
bun add react --filter apps/web

Workspace 支持很扎实,且已显著改善。与 pnpm 相比的主要差距是 Bun 的 workspace 依赖解析不够严格——pnpm 的严格性对 monorepo 来说是一个特性,因为它能捕获幽灵依赖。

与现有项目的兼容性#

你可以将 bun install 直接用于几乎任何现有的 Node.js 项目。它读取 package.json,尊重 .npmrc 的注册表配置,并正确处理 peerDependencies。过渡通常是:

bash
# 步骤 1:删除现有的 lockfile 和 node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# 步骤 2:使用 Bun 安装
bun install
 
# 步骤 3:验证你的应用是否仍然正常工作
bun run dev
# 或者: node dist/server.js(Bun 包管理器,Node 运行时)

我在十几个项目上做过这个操作,包管理器本身没有出过任何问题。唯一的坑是如果你的 CI 流水线专门查找 package-lock.json——你需要更新它来处理 bun.lockb

Node.js 兼容性#

这是我必须最谨慎的部分,因为情况每个月都在变化。截至 2026 年初,以下是诚实的现状。

能用的#

绝大多数 npm 包无需修改即可工作。Bun 实现了大多数 Node.js 内置模块:

typescript
// 这些在 Bun 中都能按预期工作
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";

CommonJS 和 ESM 都能用。require()import 可以共存。TypeScript 无需任何编译步骤直接运行——Bun 在解析时剥离类型。

能用的框架:

  • Express — 能用,包括中间件生态
  • Fastify — 能用
  • Hono — 能用(而且和 Bun 配合非常出色)
  • Next.js — 能用,但有注意事项(下面详述)
  • Prisma — 能用
  • Drizzle ORM — 能用
  • Socket.io — 能用

不能用的(或有问题的)#

差距往往集中在几个类别:

原生插件(node-gyp):如果一个包使用了用 node-gyp 编译的 C++ 插件,它可能在 Bun 下不能用。Bun 有自己的 FFI 系统并支持许多原生模块,但覆盖率不是 100%。例如,bcrypt(原生版本)曾出现问题——请改用 bcryptjs

bash
# 检查一个包是否使用原生插件
ls node_modules/your-package/binding.gyp  # 如果这个文件存在,就是原生的

特定的 Node.js 内部实现:一些包深入使用了 Node.js 内部接口如 process.binding() 或使用 V8 特定的 API。这些在 Bun 中不能用,因为它运行在 JavaScriptCore 上。

typescript
// 这在 Bun 中不能用——V8 特有
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// 这能用——使用 Bun 的等价物或跨运行时方法
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker threads:Bun 支持 Web Workers 和 node:worker_threads,但存在边界情况。一些高级用法模式——特别是围绕 SharedArrayBufferAtomics 的——行为可能不同。

vm 模块node:vm 有部分支持。如果你的代码或依赖大量使用 vm.createContext()(一些模板引擎会),请彻底测试。

兼容性追踪器#

Bun 维护着一个官方兼容性追踪器。在为项目承诺使用 Bun 之前检查它:

bash
# 对你的项目运行 Bun 内置的兼容性检查
bun --bun node_modules/.bin/your-tool
 
# --bun 标志强制对 node_modules 脚本也使用 Bun 运行时

我的建议:不要假设兼容。在决定之前先在 Bun 下运行你的测试套件。只需要五分钟,却能节省数小时的调试时间。

bash
# 快速兼容性检查——在 Bun 下运行你的完整测试套件
bun test  # 如果你使用 bun test runner
# 或者
bun run vitest  # 如果你使用 vitest

Bun 的内置 API#

这才是 Bun 变得有趣的地方。Bun 不仅仅是重新实现 Node.js 的 API,它还提供了自己的 API,设计得更简洁、更快速。

Bun.serve()——内置 HTTP 服务器#

这是我用得最多的 API。它简洁、快速,WebSocket 支持开箱即用。

typescript
const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
 
    if (url.pathname === "/") {
      return new Response("Hello from Bun!", {
        headers: { "Content-Type": "text/plain" },
      });
    }
 
    if (url.pathname === "/api/users") {
      const users = [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ];
      return Response.json(users);
    }
 
    return new Response("Not Found", { status: 404 });
  },
});
 
console.log(`Server running at http://localhost:${server.port}`);

注意几个要点:

  1. Web 标准的 Request/Response — 没有专有 API。fetch 处理器接收标准的 Request 并返回标准的 Response。如果你写过 Cloudflare Worker,会感觉完全一样。
  2. Response.json() — 内置的 JSON 响应辅助函数。
  3. 无需导入Bun.serve 是全局的。不需要 require("http")

这是一个更真实的例子,包含路由、JSON 请求体解析和错误处理:

typescript
import { Database } from "bun:sqlite";
 
const db = new Database("app.db");
db.run(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    completed INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
const server = Bun.serve({
  port: process.env.PORT || 3000,
 
  async fetch(req) {
    const url = new URL(req.url);
    const method = req.method;
 
    try {
      // GET /api/todos
      if (url.pathname === "/api/todos" && method === "GET") {
        const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
        return Response.json(todos);
      }
 
      // POST /api/todos
      if (url.pathname === "/api/todos" && method === "POST") {
        const body = await req.json();
 
        if (!body.title || typeof body.title !== "string") {
          return Response.json({ error: "Title is required" }, { status: 400 });
        }
 
        const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
        const todo = stmt.get(body.title);
        return Response.json(todo, { status: 201 });
      }
 
      // DELETE /api/todos/:id
      const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
      if (deleteMatch && method === "DELETE") {
        const id = parseInt(deleteMatch[1], 10);
        db.run("DELETE FROM todos WHERE id = ?", [id]);
        return new Response(null, { status: 204 });
      }
 
      return Response.json({ error: "Not found" }, { status: 404 });
    } catch (error) {
      console.error("Request error:", error);
      return Response.json({ error: "Internal server error" }, { status: 500 });
    }
  },
});
 
console.log(`Server running on port ${server.port}`);

这就是大约 50 行代码实现的带 SQLite 的完整 CRUD API。没有 Express,没有 ORM,没有中间件链。对于小型 API 和内部工具,这已经是我的首选方案了。

Bun.file() 和 Bun.write()——文件 I/O#

Bun 的文件 API 比 fs.readFile() 简洁得令人耳目一新:

typescript
// 读取文件
const file = Bun.file("./config.json");
const text = await file.text();       // 读取为字符串
const json = await file.json();       // 直接解析为 JSON
const bytes = await file.arrayBuffer(); // 读取为 ArrayBuffer
const stream = file.stream();          // 读取为 ReadableStream
 
// 文件元数据
console.log(file.size);  // 字节大小
console.log(file.type);  // MIME 类型(例如 "application/json")
 
// 写入文件
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
 
// 将 Response body 写入文件
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

Bun.file() API 是惰性的——在你调用 .text().json() 等方法之前不会读取文件。这意味着你可以传递 Bun.file() 引用而不产生 I/O 开销,直到你真正需要数据时才读取。

内置 WebSocket 支持#

WebSocket 在 Bun.serve() 中是一等公民:

typescript
const server = Bun.serve({
  port: 3000,
 
  fetch(req, server) {
    const url = new URL(req.url);
 
    if (url.pathname === "/ws") {
      const upgraded = server.upgrade(req, {
        data: {
          userId: url.searchParams.get("userId"),
          joinedAt: Date.now(),
        },
      });
 
      if (!upgraded) {
        return new Response("WebSocket upgrade failed", { status: 400 });
      }
      return undefined;
    }
 
    return new Response("Use /ws for WebSocket connections");
  },
 
  websocket: {
    open(ws) {
      console.log(`Client connected: ${ws.data.userId}`);
      ws.subscribe("chat");
    },
 
    message(ws, message) {
      // 广播给所有订阅者
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

server.publish()ws.subscribe() 模式是内置的发布/订阅。不需要 Redis,不需要单独的 WebSocket 库。对于简单的实时功能,这非常方便。

内置 SQLite(bun:sqlite)#

这是最让我惊讶的。Bun 在运行时中直接内置了 SQLite:

typescript
import { Database } from "bun:sqlite";
 
// 打开或创建数据库
const db = new Database("myapp.db");
 
// WAL 模式以获得更好的并发读取性能
db.exec("PRAGMA journal_mode = WAL");
 
// 创建表
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
// 预编译语句(可复用,重复查询更快)
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
 
const findByEmail = db.prepare(
  "SELECT * FROM users WHERE email = $email"
);
 
// 使用
const user = insertUser.get({
  $email: "alice@example.com",
  $name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
 
// 事务
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
  for (const user of users) {
    insertUser.run({ $email: user.email, $name: user.name });
  }
  return users.length;
});
 
const count = insertMany([
  { email: "bob@example.com", name: "Bob" },
  { email: "carol@example.com", name: "Carol" },
]);
console.log(`Inserted ${count} users`);

这是同步的 SQLite,具有 C 库的性能(因为它就是——Bun 直接嵌入了 libsqlite3)。对于 CLI 工具、本地优先应用和小型服务,内置 SQLite 意味着你的数据层零外部依赖。

Bun 测试运行器#

bun test 在大多数情况下是 Jest 的直接替代品。它使用相同的 describe/it/expect API 并支持大多数 Jest 匹配器。

基本用法#

typescript
// math.test.ts
import { describe, it, expect } from "bun:test";
 
describe("math utilities", () => {
  it("adds numbers correctly", () => {
    expect(1 + 2).toBe(3);
  });
 
  it("handles floating point", () => {
    expect(0.1 + 0.2).toBeCloseTo(0.3);
  });
});
bash
# 运行所有测试
bun test
 
# 运行特定文件
bun test math.test.ts
 
# 运行匹配模式的测试
bun test --test-name-pattern "adds numbers"
 
# 监听模式
bun test --watch
 
# 覆盖率
bun test --coverage

Mock#

Bun 支持与 Jest 兼容的 mock:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Mock 一个模块
mock.module("./database", () => ({
  query: mock(() => [{ id: 1, name: "Alice" }]),
}));
 
describe("fetchUsers", () => {
  it("returns users from database", async () => {
    const users = await fetchUsers();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe("Alice");
  });
});
 
// Spy 一个对象方法
describe("console", () => {
  it("tracks console.log calls", () => {
    const logSpy = spyOn(console, "log");
    console.log("test message");
    expect(logSpy).toHaveBeenCalledWith("test message");
    logSpy.mockRestore();
  });
});

Bun Test vs. Vitest——我的诚实比较#

我在这个项目(以及大多数项目)中使用 Vitest。以下是我还没有完全切换的原因:

bun test 胜出的地方:

  • 启动速度。bun test 开始执行测试时,Vitest 还没加载完配置。
  • 零配置。基本设置不需要 vitest.config.ts
  • 内置 TypeScript。不需要转换步骤。

Vitest 仍然胜出的地方:

  • 生态系统:Vitest 有更多插件、更好的 IDE 集成和更大的社区。
  • 配置:Vitest 的配置系统更灵活。自定义 reporter、复杂的 setup 文件、多测试环境。
  • 浏览器模式:Vitest 可以在真实浏览器中运行测试。Bun 不能。
  • 兼容性:一些测试库(Testing Library、MSW)在 Vitest/Jest 上经过了更彻底的测试。
  • 快照测试:两者都支持,但 Vitest 的实现更成熟,diff 输出更好。

对于测试需求简单的新项目,我会用 bun test。对于已有 Testing Library、MSW 和复杂 mock 的成熟项目,我继续用 Vitest。

Bun 打包器#

bun build 是一个快速的 JavaScript/TypeScript 打包器。它不是 webpack 的替代品——更像是 esbuild 的同类:快速、有主见,专注于常见场景。

基本打包#

bash
# 打包单个入口点
bun build ./src/index.ts --outdir ./dist
 
# 为不同目标打包
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
 
# 压缩
bun build ./src/index.ts --outdir ./dist --minify
 
# 生成 sourcemap
bun build ./src/index.ts --outdir ./dist --sourcemap external

编程式 API#

typescript
const result = await Bun.build({
  entrypoints: ["./src/index.ts", "./src/worker.ts"],
  outdir: "./dist",
  target: "browser",
  minify: {
    whitespace: true,
    identifiers: true,
    syntax: true,
  },
  splitting: true,    // 代码分割
  sourcemap: "external",
  external: ["react", "react-dom"],  // 不打包这些
  naming: "[dir]/[name]-[hash].[ext]",
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
});
 
if (!result.success) {
  console.error("Build failed:");
  for (const log of result.logs) {
    console.error(log);
  }
  process.exit(1);
}
 
for (const output of result.outputs) {
  console.log(`${output.path} — ${output.size} bytes`);
}

Tree-Shaking#

Bun 支持 ESM 的 tree-shaking:

typescript
// utils.ts
export function used() {
  return "I'll be in the bundle";
}
 
export function unused() {
  return "I'll be tree-shaken away";
}
 
// index.ts
import { used } from "./utils";
console.log(used());
bash
bun build ./src/index.ts --outdir ./dist --minify
# `unused` 函数不会出现在输出中

Bun Build 的不足之处#

  • 不支持 CSS 打包 — 你需要单独的工具处理 CSS(PostCSS、Lightning CSS、Tailwind CLI)。
  • 不生成 HTML — 它打包 JavaScript/TypeScript,而不是完整的 Web 应用。
  • 插件生态 — esbuild 有更大的插件生态系统。Bun 的插件 API 兼容但社区更小。
  • 高级代码分割 — Webpack 和 Rollup 仍然提供更复杂的分块策略。

对于构建库或简单 Web 应用的 JS 包,bun build 非常出色。对于需要 CSS modules、图片优化和自定义分块策略的复杂应用构建,你仍然需要一个完整的打包器。

Bun Macros#

一个真正独特的功能:通过宏实现编译时代码执行。

typescript
// build-info.ts——这个文件在构建时运行,不是运行时
export function getBuildInfo() {
  return {
    builtAt: new Date().toISOString(),
    gitSha: require("child_process")
      .execSync("git rev-parse --short HEAD")
      .toString()
      .trim(),
    nodeVersion: process.version,
  };
}
typescript
// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
 
// getBuildInfo() 在打包时执行
// 结果作为静态值内联
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

打包后,getBuildInfo() 被替换为字面量对象——运行时没有函数调用,没有 child_process 的导入。代码在构建期间运行,结果被内联。这对于嵌入构建元数据、功能开关或特定于环境的配置非常强大。

在 Next.js 中使用 Bun#

这是我被问得最多的问题,所以我来非常具体地说明。

现在能用的#

Bun 作为 Next.js 的包管理器——完美运行:

bash
# 使用 Bun 安装依赖,然后使用 Node.js 运行 Next.js
bun install
bun run dev    # 这实际上默认通过 Node.js 运行 "dev" 脚本
bun run build
bun run start

这是我在每个 Next.js 项目上做的事情。bun run <script> 命令读取 package.jsonscripts 部分并执行它。默认情况下,它使用系统的 Node.js 进行实际执行。你获得了 Bun 快速包安装的优势,而不改变你的运行时。

Bun 运行时用于 Next.js 开发:

bash
# 强制 Next.js 在 Bun 运行时下运行
bun --bun run dev

这在大多数情况下都能用于开发。--bun 标志告诉 Bun 使用自己的运行时而不是委托给 Node.js。热模块替换能用。API 路由能用。Server Components 能用。

仍在实验阶段的#

Bun 运行时用于 Next.js 生产构建:

bash
# 使用 Bun 运行时构建
bun --bun run build
 
# 使用 Bun 运行时启动生产服务器
bun --bun run start

这对许多项目能用,但我遇到过一些边界情况:

  1. 一些中间件行为不同 — 如果你使用了依赖 Node.js 特有 API 的 Next.js 中间件,可能会遇到兼容性问题。
  2. 图片优化 — Next.js 的图片优化管道使用 sharp,这是一个原生插件。它与 Bun 兼容,但我偶尔见过问题。
  3. ISR(增量静态再生) — 能用,但我没有在 Bun 下的生产环境中进行过压力测试。

我对 Next.js 的建议#

使用 Bun 作为包管理器。使用 Node.js 作为运行时。这样你获得了 bun install 的速度优势而没有任何兼容性风险。

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# 日常工作流
bun install      # 快速包安装
bun run dev      # 通过 Node.js 运行 "next dev"
bun run build    # 通过 Node.js 运行 "next build"

当 Bun 的 Node.js 兼容性达到 Next.js 内部使用所需的 100% 时(已经接近了,但还没到),我会切换。在那之前,光是包管理器就足以节省我的时间来证明安装的价值。

Docker 与 Bun#

官方 Bun Docker 镜像维护良好,已经可以在生产中使用。

基本 Dockerfile#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# 安装依赖
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# 构建(如果需要)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
 
# 生产
FROM base AS production
WORKDIR /app
 
# 不要以 root 身份运行
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
 
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]

多阶段构建实现最小镜像#

dockerfile
# 构建阶段:包含所有依赖的完整 Bun 镜像
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
 
# 运行时阶段:更小的基础镜像
FROM oven/bun:1-slim AS runtime
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/dist ./dist
 
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]

编译为单一二进制文件#

这是 Bun 的一个杀手级部署功能:

bash
# 将你的应用编译为单一可执行文件
bun build --compile ./src/server.ts --outfile server
 
# 输出是独立的二进制文件——运行不需要 Bun 或 Node.js
./server
dockerfile
# 使用编译后的二进制文件实现超小 Docker 镜像
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
 
# 最终镜像——只有二进制文件
FROM debian:bookworm-slim
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/server ./server
 
EXPOSE 3000
CMD ["./server"]

编译后的二进制文件通常为 50-90 MB(它捆绑了 Bun 运行时)。这比 Go 二进制文件大,但比完整的 Node.js 安装加上 node_modules 小得多。对于容器化部署,这种自包含特性是一个显著的简化。

大小比较#

bash
# Node.js 镜像
docker images | grep node
# node:20-slim    ~180MB
 
# Bun 镜像
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# 编译后的二进制文件基于 debian:bookworm-slim
# ~80MB 基础 + ~70MB 二进制 = ~150MB 总计
 
# vs. Alpine + Node.js
# node:20-alpine   ~130MB + node_modules

二进制方法彻底消除了最终镜像中的 node_modules。生产中不需要 npm install。不需要数百个包带来的供应链攻击面。只有一个文件。

迁移模式#

如果你正在考虑迁移到 Bun,以下是我推荐的渐进路径:

阶段 1:仅包管理器(零风险)#

bash
# 将 npm/yarn/pnpm 替换为 bun install
# 修改你的 CI 流水线:
# 之前:
npm ci
 
# 之后:
bun install --frozen-lockfile

无需代码更改。无需运行时更改。只是更快的安装。如果出了什么问题(不会的),删除 bun.lockb 然后运行 npm install 即可回退。

阶段 2:脚本和工具#

bash
# 使用 bun 运行开发脚本
bun run dev
bun run lint
bun run format
 
# 使用 bun 运行一次性脚本
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

仍然使用 Node.js 作为实际应用程序的运行时。但脚本受益于 Bun 更快的启动和原生 TypeScript 支持。

阶段 3:测试运行器(中等风险)#

bash
# 对简单的测试套件将 vitest/jest 替换为 bun test
bun test
 
# 对复杂的测试设置保留 vitest
# (Testing Library、MSW、自定义环境)

bun test 下运行你的完整测试套件。如果全部通过,你就消除了一个 devDependency。如果由于兼容性问题有些测试失败,保留 Vitest 用于那些测试,其余的使用 bun test

阶段 4:新服务的运行时(可控风险)#

typescript
// 新的微服务或 API——从第一天起就用 Bun
Bun.serve({
  port: 3000,
  fetch(req) {
    // 你的新服务写在这里
  },
});

不要将现有的 Node.js 服务迁移到 Bun 运行时。相反,从一开始就用 Bun 编写新服务。这样能限制你的爆炸半径。

阶段 5:运行时迁移(高级)#

bash
# 只有在经过充分测试之后:
# 将现有服务的 node 替换为 bun
# 之前:
node dist/server.js
 
# 之后:
bun dist/server.js

我只建议对具有出色测试覆盖率的服务这样做。在切换生产之前,在 Bun 下运行你的负载测试。

环境变量和配置#

Bun 自动处理 .env 文件——不需要 dotenv 包:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// 无需任何导入即可使用
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun 特有的替代方式

Bun 自动加载 .env.env.local.env.production 等,遵循与 Next.js 相同的约定。package.json 中又少了一个依赖。

错误处理和调试#

Bun 的错误输出已经显著改善,但在某些情况下仍不如 Node.js 完善:

bash
# Bun 的调试器——与 VS Code 配合使用
bun --inspect run server.ts
 
# Bun 的 inspect-brk——在第一行暂停
bun --inspect-brk run server.ts

对于 VS Code,在 .vscode/launch.json 中添加:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "bun",
      "request": "launch",
      "name": "Debug Bun",
      "program": "${workspaceFolder}/src/server.ts",
      "cwd": "${workspaceFolder}",
      "stopOnEntry": false,
      "watchMode": false
    }
  ]
}

Bun 中的堆栈跟踪通常是准确的,并且包含 TypeScript 的 source map。主要的调试差距是一些 Node.js 特定的调试工具(如 ndbclinic.js)在 Bun 下不能用。

安全考虑#

如果你正在评估 Bun 用于生产环境,有几点需要考虑:

成熟度:Node.js 已经在生产环境运行了 15 年以上。HTTP 解析、TLS 处理和流处理中的每个边界情况都已被发现和修复。Bun 更年轻。它经过了良好的测试,但未发现 bug 的面更大。

安全补丁:Bun 团队频繁发布更新,但 Node.js 安全团队有正式的 CVE 流程、协调披露机制和更长的跟踪记录。对于安全关键的应用程序,这很重要。

供应链:Bun 的内置功能(SQLite、HTTP 服务器、WebSocket)意味着更少的 npm 依赖。更少的依赖意味着更小的供应链攻击面。这是一个真正的安全优势。

bash
# 比较依赖数量
# 一个典型的 Express + SQLite + WebSocket 项目:
npm ls --all | wc -l
# ~340 个包
 
# 使用 Bun 内置功能实现相同功能:
bun pm ls --all | wc -l
# ~12 个包(只有你的应用代码)

这是你信任用于生产工作负载的包数量的有意义的减少。

性能调优#

一些 Bun 特有的性能技巧:

typescript
// 使用 Bun.serve() 选项进行生产调优
Bun.serve({
  port: 3000,
 
  // 增加最大请求体大小(默认 128MB)
  maxRequestBodySize: 1024 * 1024 * 50, // 50MB
 
  // 启用开发模式以获得更好的错误页面
  development: process.env.NODE_ENV !== "production",
 
  // 端口重用(对零停机重启有用)
  reusePort: true,
 
  fetch(req) {
    return new Response("OK");
  },
});
typescript
// 使用 Bun.Transpiler 进行运行时代码转换
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  target: "browser",
});
 
const code = transpiler.transformSync(`
  const App: React.FC = () => <div>Hello</div>;
  export default App;
`);
bash
# Bun 的内存使用标志
bun --smol run server.ts  # 减少内存占用(稍慢)
 
# 设置最大堆大小
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512MB 限制

常见坑#

使用 Bun 一年后,以下是让我踩过坑的东西:

1. 全局 Fetch 行为不同#

typescript
// Node.js 18+ 的 fetch 和 Bun 的 fetch 在处理
// 某些 header 和重定向时略有不同
 
// Bun 默认跟随重定向(像浏览器一样)
// Node.js 的 fetch 也跟随重定向,但在某些状态码
// (303、307、308)上行为可能不同
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // 明确指定重定向处理方式
});

2. 进程退出行为#

typescript
// 当事件循环为空时 Bun 会退出
// Node.js 有时由于残留的 handle 而继续运行
 
// 如果你的 Bun 脚本意外退出,说明没有东西
// 在保持事件循环活跃
 
// 这在 Bun 中会立即退出:
setTimeout(() => {}, 0);
 
// 这会保持运行:
setTimeout(() => {}, 1000);
// (Bun 在 timeout 触发后退出)

3. TypeScript 配置#

typescript
// Bun 有自己的 tsconfig 默认值
// 如果你在 Bun 和 Node.js 之间共享项目,
// 在 tsconfig.json 中明确指定:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // 添加 Bun 类型定义
  }
}
bash
# 安装 Bun 类型
bun add -d @types/bun

4. 开发中的热重载#

bash
# Bun 有内置的监听模式
bun --watch run server.ts
 
# 这在文件更改时重启进程
# 这不是 HMR(热模块替换)——是完整重启
# 但因为 Bun 启动得太快,感觉是即时的

5. bunfig.toml 配置文件#

toml
# bunfig.toml——Bun 的配置文件(可选)
 
[install]
# 使用私有注册表
registry = "https://npm.mycompany.com"
 
# 作用域注册表
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# 测试配置
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# bun run 使用的 shell
shell = "bash"

我的结论#

使用一年后,以下是我最终的定位:

我现在在哪里使用 Bun#

所有项目的包管理器 — 包括这个 Next.js 博客。bun install 更快,兼容性基本完美。我看不到再使用 npm 或 yarn 的理由了。pnpm 是我唯一会考虑的替代方案(因为它在 monorepo 中严格的依赖解析)。

脚本和 CLI 工具的运行时 — 任何我需要运行一次的 TypeScript 文件,都用 bun 运行。无需编译步骤。快速启动。内置 .env 加载。它已经完全取代了我工作流中的 ts-nodetsx

小型 API 和内部工具的运行时Bun.serve() + bun:sqlite 对于内部工具、webhook 处理器和小型服务来说是一个生产力极高的技术栈。"一个二进制文件,零依赖"的部署模型非常有吸引力。

简单项目的测试运行器 — 对于测试需求直接的项目,bun test 快速且零配置。

我仍然使用 Node.js 的地方#

生产环境的 Next.js — 不是因为 Bun 不能用,而是因为风险回报比还不够。Next.js 是一个复杂的框架,有许多集成点。我希望在它下面运行的是经过最充分实战检验的运行时。

关键生产服务 — 我的主要 API 服务器在 PM2 后面运行 Node.js。监控生态系统、调试工具、运维知识——都是围绕 Node.js 的。Bun 会达到那个水平的,但现在还没到。

任何有原生插件的东西 — 如果依赖链中包含 C++ 原生插件,我甚至不会尝试 Bun。不值得调试兼容性问题。

不熟悉 Bun 的团队 — 向从未使用过 Bun 的团队引入 Bun 作为运行时会增加认知负担。作为包管理器,可以。作为运行时,等团队准备好了再说。

我在关注什么#

Bun 的兼容性追踪器 — 当它在我关心的 Node.js API 上达到 100% 时,我会重新评估。

框架支持 — Next.js、Remix 和 SvelteKit 都有不同程度的 Bun 支持。当其中一个正式支持 Bun 作为生产运行时,那就是一个信号。

企业采用 — 一旦有真实 SLA 的公司在生产中运行 Bun 并撰写相关文章,成熟度问题就得到了回答。

1.2+ 版本线 — Bun 发展很快。功能每周都在落地。我今天使用的 Bun 比一年前尝试的 Bun 有了实质性的改善。

总结#

Bun 不是银弹。它不会让一个慢的应用变快,也不会让一个设计糟糕的 API 变得设计良好。但它确实是 JavaScript 生态系统中开发者体验的真正改善。

我最欣赏 Bun 的不是任何单一功能。而是工具链复杂度的降低。一个二进制文件安装包、运行 TypeScript、打包代码、运行测试。脚本不需要 tsconfig.json。不需要 Babel。不需要单独的测试运行器配置。只需 bun run your-file.ts,它就能工作。

实用建议:从 bun install 开始。零风险,立竿见影。然后尝试 bun run 运行脚本。然后根据你的具体需求评估其余部分。你不需要全面投入。Bun 完全可以作为部分替代来使用,而这可能是大多数人今天应该使用它的方式。

相关文章