Bun 实战:什么好使、什么不行、什么出乎意料
Bun 作为运行时、包管理器、打包器和测试运行器的真实使用体验。真实基准测试、Node.js 兼容性缺口、迁移模式,以及我今天在生产中使用 Bun 的场景。
每隔几年,JavaScript 生态系统就会出现一个新运行时,随之而来的讨论总是遵循着可预见的轨迹。炒作。基准测试。"X 已死。"现实检验。最终沉淀到新工具真正擅长的实际用例。
Bun 正处于这条轨迹的中间阶段。与大多数挑战者不同,它留了下来。不是因为它"更快"(虽然它确实经常更快),而是因为它在解决一个真正不同的问题:JavaScript 工具链有太多活动部件,Bun 把它们合并成了一个。
我已经在各种场景下使用 Bun 一年多了。一部分是在生产环境。一部分是替换了我以为永远不会替换的工具。这篇文章是对什么好用、什么不好用、以及差距仍然重要的领域的诚实记录。
Bun 到底是什么#
首先需要纠正一个误解:Bun 不是"更快的 Node.js"。这种说法低估了它。
Bun 是四个工具合一的单一二进制文件:
- JavaScript/TypeScript 运行时 — 运行你的代码,类似 Node.js 或 Deno
- 包管理器 — 替代 npm、yarn 或 pnpm
- 打包器 — 在某些场景下替代 esbuild、webpack 或 Rollup
- 测试运行器 — 替代大多数测试套件中的 Jest 或 Vitest
与 Node.js 的关键架构差异在于引擎。Node.js 使用 V8(Chrome 的引擎)。Bun 使用 JavaScriptCore(Safari 的引擎)。两者都是成熟的、生产级的引擎,但它们做出了不同的权衡。JavaScriptCore 往往有更快的启动时间和更低的内存开销。V8 往往在长时间运行的计算中有更好的峰值吞吐量。实际上,对于大多数工作负载,这些差异比你想象的要小。
另一个主要差异点:Bun 是用 Zig 编写的,一种系统编程语言,大致处于与 C 相同的层级,但具有更好的内存安全保障。这就是 Bun 能够在性能上如此激进的原因——Zig 提供了 C 所给予的那种底层控制能力,但没有 C 那样密集的陷阱。
# 检查你的 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 最大的真实优势,而且差距明显。
# 测量启动时间——各运行 100 次
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# 典型结果:
# node: ~40ms
# bun: ~6ms大约是 6-7 倍的启动时间差异。对于脚本、CLI 工具以及冷启动很重要的 Serverless 函数来说,这很显著。对于启动一次然后运行数周的长期运行服务器进程来说,这无关紧要。
包安装#
这是 Bun 碾压竞争对手的另一个领域。
# 全新安装基准测试——先删除 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 倍的差异,而且非常稳定。原因主要是:
- 二进制 lockfile —
bun.lockb是二进制格式,不是 JSON。读写更快。 - 全局缓存 — Bun 维护全局模块缓存,跨项目的重复安装共享已下载的包。
- Zig 的 I/O — 包管理器本身是用 Zig 编写的,不是 JavaScript。文件 I/O 操作更接近底层。
- 符号链接策略 — Bun 使用硬链接到全局缓存,而不是复制文件。
HTTP 服务器吞吐量#
Bun 内置的 HTTP 服务器很快,但比较需要上下文。
# 用 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,000Bun vs. 原生 Node.js:对于简单响应大约快 2 倍。Bun vs. Express:大约快 7 倍,但这不公平,因为 Express 增加了中间件开销。一旦你加入真实逻辑——数据库查询、认证、实际数据的 JSON 序列化——差距就急剧缩小了。
差异可忽略的场景#
CPU 密集型计算:
// 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`);bun run fibonacci.ts # ~1650ms
node fibonacci.ts # ~1580msNode.js(V8)在这里实际上略胜一筹。V8 的 JIT 编译器在热循环上更加激进。对于 CPU 密集型工作,引擎差异基本打平——有时 V8 赢,有时 JSC 赢,差异在噪声范围内。
如何运行自己的基准测试#
不要相信任何人的基准测试,包括我的。以下是如何测量对你具体工作负载真正重要的指标:
# 安装 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。它就是更快,而且兼容性非常好。
基础用法#
# 从 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 来了解发生了什么变化。这比速度倡导者愿意承认的更重要。
# 你可以将 lockfile 转储为人类可读格式
bun bun.lockb > lockfile-dump.txt
# 或者使用内置的文本输出
bun install --yarn
# 这会在 bun.lockb 旁边生成一个 yarn.lock我的做法:将 bun.lockb 提交到仓库,同时生成一个 yarn.lock 或 package-lock.json 作为可读备份。双重保险。
Workspace 支持#
Bun 支持 npm/yarn 风格的 workspace:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# 安装所有 workspace 的依赖
bun install
# 在特定 workspace 中运行脚本
bun run --filter packages/shared build
# 向特定 workspace 添加依赖
bun add react --filter apps/webWorkspace 支持很扎实,且已显著改善。与 pnpm 相比的主要差距是 Bun 的 workspace 依赖解析不够严格——pnpm 的严格性对 monorepo 来说是一个特性,因为它能捕获幽灵依赖。
与现有项目的兼容性#
你可以将 bun install 直接用于几乎任何现有的 Node.js 项目。它读取 package.json,尊重 .npmrc 的注册表配置,并正确处理 peerDependencies。过渡通常是:
# 步骤 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 内置模块:
// 这些在 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。
# 检查一个包是否使用原生插件
ls node_modules/your-package/binding.gyp # 如果这个文件存在,就是原生的特定的 Node.js 内部实现:一些包深入使用了 Node.js 内部接口如 process.binding() 或使用 V8 特定的 API。这些在 Bun 中不能用,因为它运行在 JavaScriptCore 上。
// 这在 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,但存在边界情况。一些高级用法模式——特别是围绕 SharedArrayBuffer 和 Atomics 的——行为可能不同。
vm 模块:node:vm 有部分支持。如果你的代码或依赖大量使用 vm.createContext()(一些模板引擎会),请彻底测试。
兼容性追踪器#
Bun 维护着一个官方兼容性追踪器。在为项目承诺使用 Bun 之前检查它:
# 对你的项目运行 Bun 内置的兼容性检查
bun --bun node_modules/.bin/your-tool
# --bun 标志强制对 node_modules 脚本也使用 Bun 运行时我的建议:不要假设兼容。在决定之前先在 Bun 下运行你的测试套件。只需要五分钟,却能节省数小时的调试时间。
# 快速兼容性检查——在 Bun 下运行你的完整测试套件
bun test # 如果你使用 bun test runner
# 或者
bun run vitest # 如果你使用 vitestBun 的内置 API#
这才是 Bun 变得有趣的地方。Bun 不仅仅是重新实现 Node.js 的 API,它还提供了自己的 API,设计得更简洁、更快速。
Bun.serve()——内置 HTTP 服务器#
这是我用得最多的 API。它简洁、快速,WebSocket 支持开箱即用。
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}`);注意几个要点:
- Web 标准的 Request/Response — 没有专有 API。
fetch处理器接收标准的Request并返回标准的Response。如果你写过 Cloudflare Worker,会感觉完全一样。 Response.json()— 内置的 JSON 响应辅助函数。- 无需导入 —
Bun.serve是全局的。不需要require("http")。
这是一个更真实的例子,包含路由、JSON 请求体解析和错误处理:
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() 简洁得令人耳目一新:
// 读取文件
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() 中是一等公民:
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:
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 匹配器。
基本用法#
// 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);
});
});# 运行所有测试
bun test
# 运行特定文件
bun test math.test.ts
# 运行匹配模式的测试
bun test --test-name-pattern "adds numbers"
# 监听模式
bun test --watch
# 覆盖率
bun test --coverageMock#
Bun 支持与 Jest 兼容的 mock:
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 的同类:快速、有主见,专注于常见场景。
基本打包#
# 打包单个入口点
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#
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:
// 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());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#
一个真正独特的功能:通过宏实现编译时代码执行。
// 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,
};
}// 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 的包管理器——完美运行:
# 使用 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.json 的 scripts 部分并执行它。默认情况下,它使用系统的 Node.js 进行实际执行。你获得了 Bun 快速包安装的优势,而不改变你的运行时。
Bun 运行时用于 Next.js 开发:
# 强制 Next.js 在 Bun 运行时下运行
bun --bun run dev这在大多数情况下都能用于开发。--bun 标志告诉 Bun 使用自己的运行时而不是委托给 Node.js。热模块替换能用。API 路由能用。Server Components 能用。
仍在实验阶段的#
Bun 运行时用于 Next.js 生产构建:
# 使用 Bun 运行时构建
bun --bun run build
# 使用 Bun 运行时启动生产服务器
bun --bun run start这对许多项目能用,但我遇到过一些边界情况:
- 一些中间件行为不同 — 如果你使用了依赖 Node.js 特有 API 的 Next.js 中间件,可能会遇到兼容性问题。
- 图片优化 — Next.js 的图片优化管道使用 sharp,这是一个原生插件。它与 Bun 兼容,但我偶尔见过问题。
- ISR(增量静态再生) — 能用,但我没有在 Bun 下的生产环境中进行过压力测试。
我对 Next.js 的建议#
使用 Bun 作为包管理器。使用 Node.js 作为运行时。这样你获得了 bun install 的速度优势而没有任何兼容性风险。
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# 日常工作流
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#
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"]多阶段构建实现最小镜像#
# 构建阶段:包含所有依赖的完整 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 的一个杀手级部署功能:
# 将你的应用编译为单一可执行文件
bun build --compile ./src/server.ts --outfile server
# 输出是独立的二进制文件——运行不需要 Bun 或 Node.js
./server# 使用编译后的二进制文件实现超小 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 小得多。对于容器化部署,这种自包含特性是一个显著的简化。
大小比较#
# 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:仅包管理器(零风险)#
# 将 npm/yarn/pnpm 替换为 bun install
# 修改你的 CI 流水线:
# 之前:
npm ci
# 之后:
bun install --frozen-lockfile无需代码更改。无需运行时更改。只是更快的安装。如果出了什么问题(不会的),删除 bun.lockb 然后运行 npm install 即可回退。
阶段 2:脚本和工具#
# 使用 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:测试运行器(中等风险)#
# 对简单的测试套件将 vitest/jest 替换为 bun test
bun test
# 对复杂的测试设置保留 vitest
# (Testing Library、MSW、自定义环境)在 bun test 下运行你的完整测试套件。如果全部通过,你就消除了一个 devDependency。如果由于兼容性问题有些测试失败,保留 Vitest 用于那些测试,其余的使用 bun test。
阶段 4:新服务的运行时(可控风险)#
// 新的微服务或 API——从第一天起就用 Bun
Bun.serve({
port: 3000,
fetch(req) {
// 你的新服务写在这里
},
});不要将现有的 Node.js 服务迁移到 Bun 运行时。相反,从一开始就用 Bun 编写新服务。这样能限制你的爆炸半径。
阶段 5:运行时迁移(高级)#
# 只有在经过充分测试之后:
# 将现有服务的 node 替换为 bun
# 之前:
node dist/server.js
# 之后:
bun dist/server.js我只建议对具有出色测试覆盖率的服务这样做。在切换生产之前,在 Bun 下运行你的负载测试。
环境变量和配置#
Bun 自动处理 .env 文件——不需要 dotenv 包:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// 无需任何导入即可使用
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 完善:
# Bun 的调试器——与 VS Code 配合使用
bun --inspect run server.ts
# Bun 的 inspect-brk——在第一行暂停
bun --inspect-brk run server.ts对于 VS Code,在 .vscode/launch.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 特定的调试工具(如 ndb 或 clinic.js)在 Bun 下不能用。
安全考虑#
如果你正在评估 Bun 用于生产环境,有几点需要考虑:
成熟度:Node.js 已经在生产环境运行了 15 年以上。HTTP 解析、TLS 处理和流处理中的每个边界情况都已被发现和修复。Bun 更年轻。它经过了良好的测试,但未发现 bug 的面更大。
安全补丁:Bun 团队频繁发布更新,但 Node.js 安全团队有正式的 CVE 流程、协调披露机制和更长的跟踪记录。对于安全关键的应用程序,这很重要。
供应链:Bun 的内置功能(SQLite、HTTP 服务器、WebSocket)意味着更少的 npm 依赖。更少的依赖意味着更小的供应链攻击面。这是一个真正的安全优势。
# 比较依赖数量
# 一个典型的 Express + SQLite + WebSocket 项目:
npm ls --all | wc -l
# ~340 个包
# 使用 Bun 内置功能实现相同功能:
bun pm ls --all | wc -l
# ~12 个包(只有你的应用代码)这是你信任用于生产工作负载的包数量的有意义的减少。
性能调优#
一些 Bun 特有的性能技巧:
// 使用 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");
},
});// 使用 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;
`);# Bun 的内存使用标志
bun --smol run server.ts # 减少内存占用(稍慢)
# 设置最大堆大小
BUN_JSC_forceRAMSize=512000000 bun run server.ts # ~512MB 限制常见坑#
使用 Bun 一年后,以下是让我踩过坑的东西:
1. 全局 Fetch 行为不同#
// 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. 进程退出行为#
// 当事件循环为空时 Bun 会退出
// Node.js 有时由于残留的 handle 而继续运行
// 如果你的 Bun 脚本意外退出,说明没有东西
// 在保持事件循环活跃
// 这在 Bun 中会立即退出:
setTimeout(() => {}, 0);
// 这会保持运行:
setTimeout(() => {}, 1000);
// (Bun 在 timeout 触发后退出)3. TypeScript 配置#
// Bun 有自己的 tsconfig 默认值
// 如果你在 Bun 和 Node.js 之间共享项目,
// 在 tsconfig.json 中明确指定:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // 添加 Bun 类型定义
}
}# 安装 Bun 类型
bun add -d @types/bun4. 开发中的热重载#
# Bun 有内置的监听模式
bun --watch run server.ts
# 这在文件更改时重启进程
# 这不是 HMR(热模块替换)——是完整重启
# 但因为 Bun 启动得太快,感觉是即时的5. bunfig.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-node 和 tsx。
小型 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 完全可以作为部分替代来使用,而这可能是大多数人今天应该使用它的方式。