Bun 深度实战:一个 Zig 二进制如何重写整个 JavaScript 工具链——从 JavaScriptCore 内核、All-in-One 架构到生产级 HTTP 服务与测试体系的完整工程指南(2026)
2026 年,JavaScript 开发者终于可以只装一个二进制就解决"运行、打包、转译、装包、测试"五件大事。本文从 Bun 的 Zig 内核与 JavaScriptCore 引擎讲起,逐层拆解它的 All-in-One 架构,并用一整套可运行代码带你从零搭出一个生产级 HTTP 服务、嵌入式数据库与测试体系,最后给出性能调优方法论、常见排障清单与迁移决策。
一、背景:JavaScript 工具链的三座大山
如果你在 2015 年前后入行,一定经历过这样的早晨:项目刚拉下来,npm install 先跑三分钟,接着 webpack 打包两分钟,改一行代码热更新又要等十秒,写个单测还得先配 babel-jest + ts-jest + jest-environment-jsdom。工具链的复杂度,早就超过了业务逻辑本身。
(1)启动慢。 Node.js 基于 V8,每次冷启动都要经历:解析启动脚本、初始化 libuv 事件循环、加载内置模块、执行用户代码。中小型脚本还好,一旦涉及 TypeScript 转译(ts-node)、装饰器、path alias,启动开销就被放大数倍。Serverless 场景里,这份"启动税"每次冷启都要交一遍,直接导致冷启动延迟高、扩缩容迟钝。
(2)工具链碎片化。 一个现代前端项目,runtime 用 Node,bundler 用 Webpack/Rollup/Vite,transpiler 用 Babel/SWC,包管理用 npm/yarn/pnpm,测试用 Jest/Vitest,类型检查用 tsc。五个环节五套配置,版本互相掣肘,"在我机器上能跑"成了常态,CI 里光配环境就能劝退新人。更糟的是,这些工具各自维护一套 AST、一套 plugin 协议,组合爆炸式的兼容问题层出不穷。
(3)node_modules 黑洞。 npm 的嵌套依赖树 + 软链解析,往往让 node_modules 体积膨胀到数百 MB 甚至上 GB,CI 里光装依赖就要吃掉大量时间,磁盘也跟着遭殃。一次 rm -rf node_modules && npm install 成了前端圈的日课。
Bun 的出现,正是要一次性掀翻这三座大山。它的作者 Jarred Sumner 在 Stripe 做支付基础设施时,对 Node 的启动性能和 npm 的依赖解析深恶痛绝,于是 2021 年起用一门叫 Zig 的系统级语言,从零写了一个"全家桶":runtime + bundler + transpiler + package manager + test runner,全部塞进一个约几十 MB 的二进制里。
为什么是现在? 三个趋势叠加让 Bun 站上了风口:其一是 TypeScript 成了默认语言,转译不再是"可选优化"而是"必备步骤";其二是 Serverless / Edge 让冷启动成本被放大到业务层面;其三是开发者对"开箱即用"的容忍度降到低点——谁也不想再为一个 hello world 配三小时环境。Bun 把"转译是运行时的一部分"当成一等公民,恰好命中了这三个痛点。
二、核心概念:Bun 到底是什么
2.1 五个身份,一个二进制
Bun 不是"又一个 Node 替代品"那么简单。它在一开始就把自己定位成 All-in-One 工具链:
- Runtime:直接运行 JavaScript / TypeScript / JSX / TSX,无需预编译;
- Bundler:
bun build替代 Webpack/Rollup,支持 code splitting、tree shaking; - Transpiler:内置对 TS、JSX 的转译,启动即生效;
- Package Manager:
bun install兼容 package.json,速度碾压 npm; - Test Runner:
bun test自带 expect、mock、snapshot、DOM 测试。
装好 Bun 之后,你几乎可以删掉 node_modules、.babelrc、jest.config.js、ts-node 和一堆 devDependencies——当然,渐进迁移更稳妥,我们后面会讲。
2.2 JavaScriptCore 而不是 V8
这是 Bun 和 Deno 最本质的区别之一。Bun 内嵌的是 JavaScriptCore(JSC),也就是 Safari / WebKit 使用的引擎,而不是 Chrome 的 V8。
为什么选 JSC?先把两套引擎的流水线摆出来对比:
- V8 走
Ignition(解释器)+TurboFan(优化编译器)两级,启动时要先解释执行再靠采样热点升级到优化代码,冷启动要付出"先解释"的代价,且 TurboFan 的优化编译本身耗时。 - JSC 走
LLInt(低级解释器)→DFG(Data Flow Graph,中级 JIT)→FTL(Faster Than Light,基于 LLVM 的后端)三级,并且它的 字节码缓存(Bytecode Cache) 做得更激进——解析后的字节码可以直接落盘复用,下一次启动跳过解析阶段。
两个差异直接决定冷启动:
- JSC 的 字节码缓存 让"第二次及以后"的启动几乎免去解析开销;
- JSC 的 并发 GC(Concurrent GC) 在多核机器上停顿更短,对长生命周期服务更友好;
- JSC 启动 footprint 更小,对 CLI、Serverless 这类"启动一次就干活"的场景友好。
代价是:JSC 的嵌入难度高于 V8,社区资料少,这也是 Bun 早期踩坑最多的地方。但一旦稳定,收益直接体现在启动时间上。需要提醒的是,引擎快慢是高度场景相关的——长时间运行、热点充分预热后,V8 的 TurboFan 优化上限未必输给 JSC。Bun 的优势主战场在"短平快"和"冷启动敏感"的场景。
2.3 Zig:没有隐藏控制流的系统语言
Bun 用 Zig 写,而不是 C++ 或 Rust。Zig 的设计哲学是"显式优于隐式":没有隐式内存分配、没有异常、没有隐藏的控制流、没有宏魔法。对写一个需要精确控制内存、追求极致性能的运行时来说,Zig 的"裸金属感"反而更可控。
和 Rust 比,Zig 不强制所有权系统,写底层更灵活(也更容易写出 bug,内存安全要靠人盯);和 C 比,Zig 自带编译期内存安全检查(catch 必须处理、errdefer 清理)、更好的交叉编译,且编译速度极快。Bun 选择 Zig,本质上是在"性能上限"和"开发效率"之间押注了前者——代价是团队必须精通底层,否则排障会非常痛苦。
2.4 Bun 的 Node 兼容层:怎么做到"无感迁移"
很多人担心:Bun 换了引擎和语言,我的 Node 代码能跑吗?Bun 的答案是一个兼容层(compatibility shim):
- 它实现了约 90% 的 Node 内置模块(
fs、path、http、crypto、stream等)和Buffer语义; - 对 Node 原生插件(N-API / C++ addon),Bun 提供 N-API 兼容实现,大部分纯计算型 addon 能直接加载;
- Web 标准 API(
fetch、WebSocket、URL、crypto.subtle)原生内置,不再需要node-fetch/ws; process、__dirname、require等也做了适配。
这意味着大多数纯 JS 业务代码可以"零修改"在 Bun 上跑起来,真正需要改的通常是那些依赖特定 V8 行为、或深度 hook Node 内部的库。
2.5 Bun vs Deno vs Node:一张对照表
| 维度 | Node.js | Deno | Bun |
|---|---|---|---|
| JS 引擎 | V8 | V8 | JavaScriptCore |
| 实现语言 | C++ | Rust | Zig |
| TS 原生支持 | 需转译 | 原生 | 原生 |
| 包管理 | npm | 无(URL import) | 内置 bun install |
| 测试 | 第三方 | 内置 Deno.test | 内置 bun test |
| 标准库 | 少 | 丰富 | 丰富 + 内置 SQLite/S3/SQL |
| 启动速度 | 基准 | 快 | 最快(官方基准) |
注意:三者不是非此即彼。Deno 也在吸收 Bun 的思路(比如 Deno 2 的包管理),Node 也在加原生 TS 支持,生态在快速收敛。选型看团队偏好,不要盲目追新。
三、架构分析:Bun 是怎么把五件事捏在一起的
3.1 启动链路
当执行 bun run app.ts 时,实际发生了:
bun二进制启动,加载内嵌的 JSC;- 解析 entry 文件,Bun 的 resolver 根据
tsconfig.json的paths、package.json 的exports字段,解析所有 import; - 对
.ts/.tsx/.jsx文件,调用内嵌的 transpiler 即时转成 JS(走 JSC 的字节码缓存); - 执行用户代码。
关键点:Bun 的 resolver 是用 Zig 重写的,不走 Node 的 C++ 解析逻辑,所以路径解析、extension 补全(自动补 .ts/.tsx)、package exports 匹配都比 Node 快一个量级。这也是为什么 Bun 项目里 import './util' 能瞬间定位到 util.ts,而 Node 要试一圈扩展名。
3.2 Transpiler 流水线
Bun 的转译不是"调 Babel",而是自己用 Zig 实现了一个 TS/JSX 转译器,直接产出 JSC 能吃的字节码。它支持:
- TypeScript 类型擦除(不类型检查,只去类型,速度极快);
- JSX / TSX 转换,可配
jsxFactory、jsxFragment; - 顶层
await、装饰器(实验)、enum 等语法; tsconfig.json的paths与baseUrl路径别名。
注意:Bun 的转译不做类型检查——类型错误不会在运行期暴露,你要单独跑 tsc --noEmit 或 bun x tsc 做类型守卫。这是"快"的代价,也是工程上必须补的一刀。
3.3 内置 API 矩阵
Bun 在标准 Web API 之外,提供了一组 Bun.* 和 bun:* 命名空间的高性能 API:
Bun.serve():生产级 HTTP 服务器,原生支持 WebSocket、流式响应、静态文件;Bun.file()/Bun.write():面向 WebBlob/File的文件 I/O,零拷贝友好;Bun.spawn():子进程,stdout 直接是Response可流式读;bun:sqlite:内嵌 SQLite,比better-sqlite3更快;Bun.SQL:PostgreSQL / MySQL 客户端,tagged template 防注入;Bun.s3:S3 client,直连 S3 兼容对象存储;Bun.redis:内嵌 Redis 客户端;HTMLRewriter:类似 Cloudflare 的流式 HTML 改写器。
这套 API 的设计哲学是:高频 I/O 路径全部内置并优化,避免你在 npm 里再找一个库、再踩一遍它的性能坑。
3.4 包管理:全局缓存 + 硬链接
bun install 快的核心秘密:
- 全局缓存:所有下载的包存到
~/.bun/install/cache,按 content hash 去重; - 硬链接(hardlink)代替拷贝:
node_modules里的包是硬链接到全局缓存,装依赖几乎不占额外磁盘,也不重复下载; - 二进制锁文件
bun.lockb:比package-lock.json的纯文本解析快得多; - 并行解析:依赖树的解析在 Zig 层用多线程并行完成。
官方基准里 bun install 比 npm install 快 25~30 倍,比 pnpm 也明显更快(不同项目差异大,CI 里体感最明显)。
四、代码实战:从零搭一个生产级服务
4.1 五分钟跑起来
# 安装(macOS / Linux)
curl -fsSL https://bun.sh/install | bash
# 验证
bun --version
# 直接跑 TS,无需 tsc
echo 'console.log(`Hello from Bun, argv = ${Bun.argv}`)' > hi.ts
bun hi.ts
bun 直接执行 .ts,背后是 JSC 的即时 transpile,没有中间产物。
4.2 生产级 HTTP 服务:Bun.serve
下面这个例子覆盖了路由、中间件、JSON 解析、静态文件、流式响应和错误处理:
// server.ts
interface User { id: number; name: string }
const users: User[] = [
{ id: 1, name: "三哥" },
{ id: 2, name: "茄子" },
];
// 极简日志中间件:用 Response 包装器包裹下一环
function withLogger(next: (req: Request) => Response | Promise<Response>) {
return async (req: Request) => {
const start = performance.now();
const res = await next(req);
const ms = (performance.now() - start).toFixed(2);
console.log(`${req.method} ${new URL(req.url).pathname} -> ${res.status} (${ms}ms)`);
return res;
};
}
// 极简限流中间件:滑动窗口(演示用,生产换 Redis)
const hits = new Map<string, number[]>();
function withRateLimit(next: (req: Request) => Response | Promise<Response>, limit = 100, win = 60_000) {
return (req: Request) => {
const ip = req.headers.get("x-forwarded-for") ?? "local";
const now = Date.now();
const arr = (hits.get(ip) ?? []).filter((t) => now - t < win);
if (arr.length >= limit) return new Response("too many requests", { status: 429 });
arr.push(now);
hits.set(ip, arr);
return next(req);
};
}
const handler = withLogger(withRateLimit(async (req) => {
const url = new URL(req.url);
const { pathname } = url;
// 健康检查
if (pathname === "/health") {
return Response.json({ ok: true, uptime: process.uptime() });
}
// 用户列表(GET)
if (pathname === "/api/users" && req.method === "GET") {
return Response.json(users);
}
// 新建用户(POST,读取 JSON body)
if (pathname === "/api/users" && req.method === "POST") {
const body = await req.json<Partial<User>>();
if (!body.name) return new Response("name required", { status: 400 });
const user: User = { id: users.length + 1, name: body.name };
users.push(user);
return Response.json(user, { status: 201 });
}
// 静态文件:把 ./public 映射成根路径
if (pathname.startsWith("/static/")) {
const file = Bun.file(`./public/${pathname.slice(8)}`);
if (await file.exists()) return new Response(file);
return new Response("not found", { status: 404 });
}
return new Response("Not Found", { status: 404 });
}));
Bun.serve({
port: 3000,
fetch: handler,
// 开发期热重载:bun --hot server.ts
// error 兜底,避免未捕获异常让进程退出
error(error) {
console.error(error);
return new Response("Internal Error", { status: 500 });
},
});
console.log("🚀 Bun server on http://localhost:3000");
几个要点:
Response.json()是 Bun 对 Web 标准的增强,直接序列化对象并设好Content-Type;req.json<T>()支持泛型,省去手动 cast;Bun.file()返回的File可以直接作为Responsebody,内部走零拷贝,大文件也不会把内存打爆;error()钩子是生产服务的保险丝,务必加上;- 中间件就是普通函数组合,没有框架黑魔法,调试直观。
4.3 优雅关闭:别让请求在重启时丢失
let server = Bun.serve({ port: 3000, fetch: handler });
// 收到 SIGINT/SIGTERM 时先停接收新请求,等存量请求处理完
Bun.addEventListener("terminate", async () => {
console.log("shutting down...");
server.stop(true); // true = 等待进行中的请求完成
await sql.end();
process.exit(0);
});
server.stop(true) 的优雅关闭在生产滚动发布里非常重要,避免"切断正在飞的请求"。
4.4 原生 WebSocket:长连接不用第三方库
const clients = new Set<WebSocket>();
Bun.serve({
port: 3000,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const success = server.upgrade(req);
return success ? undefined : new Response("upgrade failed", { status: 500 });
}
return new Response("Hello, use WebSocket");
},
websocket: {
open(ws) {
clients.add(ws);
ws.send("welcome!");
},
message(ws, data) {
// 广播给除自己以外的所有连接
const msg = String(data);
for (const c of clients) if (c !== ws) c.send(msg);
},
close(ws) {
clients.delete(ws);
},
},
});
Bun 的 WebSocket 实现底层做了大量优化,单机轻松支撑数十万长连接,比 Node 的 ws 库吞吐高一个量级,而且 API 就是标准 WebSocket,前端无需任何适配。
4.5 文件 I/O:Bun.file / Bun.write 的零拷贝风格
// 读
const pkg = Bun.file("package.json");
const meta = await pkg.json();
console.log(meta.name, meta.version);
// 写:字符串
await Bun.write("dist/hello.txt", "你好,Bun");
// 写:从另一个文件零拷贝
const src = Bun.file("big.iso");
await Bun.write("backup/big.iso", src); // 内部走 sendfile/拷贝优化
// 管道:把子进程输出直接落盘,不进内存
const proc = Bun.spawn(["cat", "huge.log"]);
await Bun.write("out.log", proc.stdout);
Bun.write 接受 string | Blob | Response | ArrayBuffer | TypedArray,统一抽象,避免到处 fs.createWriteStream。
4.6 嵌入式 SQLite:bun:sqlite
import { Database } from "bun:sqlite";
const db = new Database("app.db", { create: true });
db.run("PRAGMA journal_mode = WAL");
db.run(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
views INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// 参数化,自动防注入
const insert = db.query("INSERT INTO posts (title) VALUES (?)");
insert.run("Bun 真香");
insert.run("Zig 也很香");
// 查询返回普通对象数组
const rows = db.query("SELECT * FROM posts ORDER BY id DESC LIMIT ?").all(10);
console.log(rows);
// 事务
const tx = db.transaction((titles: string[]) => {
for (const t of titles) insert.run(t);
});
tx(["a", "b", "c"]);
bun:sqlite 是 Bun 用 Zig 封装的 SQLite 绑定,官方基准比 better-sqlite3(Node 上最快的 SQLite 库)还快,因为省去了 Node 的 C++ 绑定开销和 JS 层的对象转换。本地缓存、会话存储、嵌入式分析这类场景,直接上 bun:sqlite 比外接一个 Redis/Postgres 更轻。
4.7 关系型数据库:Bun.SQL(PostgreSQL / MySQL)
需要真正的服务端数据库时,用 Bun.SQL,tagged template 天然防 SQL 注入:
const sql = new Bun.SQL({
url: "postgres://user:pass@localhost:5432/mydb",
connectionLimit: 10, // 连接池大小,默认按 CPU 数
});
// tagged template:变量自动参数化
const age = 18;
const teens = await sql`SELECT id, name FROM users WHERE age > ${age} ORDER BY age`;
// 事务
await sql.begin(async (tx) => {
await tx`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`;
await tx`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`;
});
// 用完关闭
await sql.end();
Bun.SQL 底层是 Bun 自己实现的 Postgres/MySQL 协议解析,不走 pg/mysql2 的 JS 实现,握手和查询路径都更短。如果你用 ORM(Prisma / Drizzle),它们也都支持把 Bun.SQL 作为底层驱动。
4.8 缓存与对象存储:Bun.redis 与 Bun.s3
// Redis:内嵌客户端
const redis = new Bun.redis("redis://localhost:6379");
await redis.set("k", "v", "EX", 60); // 60 秒过期
const v = await redis.get("k");
// S3:直传对象存储
const s3 = new Bun.S3Client({
endpoint: "https://s3.amazonaws.com",
accessKeyId: Bun.env.AWS_KEY!,
secretAccessKey: Bun.env.AWS_SECRET!,
bucket: "my-bucket",
});
await s3.write("avatars/u1.png", Bun.file("local.png"));
把高频 I/O(缓存、对象存储)也收编进运行时,是 Bun "All-in-One" 哲学的延伸——少引入一个 SDK,就少一个版本冲突源。
4.9 流式 HTML 改写:HTMLRewriter
Bun.serve({
port: 3000,
fetch() {
const res = new Response("<html><body><h1>old</h1></body></html>");
return new HTMLRewriter()
.on("h1", {
element(el) { el.setInnerContent("served by Bun"); },
})
.transform(res);
},
});
HTMLRewriter 基于流式解析,边收边改边发,不会把整页读进内存,适合做边缘注入、A/B 标签、SEO 注入。
4.10 测试:bun test 全功能
// math.test.ts
import { expect, test, describe, mock, spyOn, beforeEach } from "bun:test";
function add(a: number, b: number) { return a + b; }
describe("add", () => {
test("正数相加", () => {
expect(add(1, 2)).toBe(3);
});
test("快照测试", () => {
expect({ a: 1, list: [1, 2, 3] }).toMatchSnapshot();
});
test("mock 函数", () => {
const fn = mock((x: number) => x * 2);
fn(21);
expect(fn).toHaveBeenCalledWith(21);
expect(fn).toHaveReturnedWith(42);
});
});
// DOM 测试(需 happy-dom 或 linkedom)
import { JSDOM } from "happy-dom";
test("DOM 操作", () => {
const doc = new JSDOM(`<button id="b">click</button>`).document;
expect(doc.getElementById("b")?.textContent).toBe("click");
});
bun test 与 Jest/Vitest 的 expect API 高度兼容,迁移成本极低。它还内置 mock / spyOn、todo / skip 标记、按文件分 worker 的并行执行,以及 --coverage 覆盖率。配合 bun:sqlite 还能做数据库层测试而无需起一个外部实例。
4.11 编译成单文件二进制:bun build --compile
最实用的部署技巧之一,把 TS 服务编译成一个独立可执行文件,目标机不需要装 Bun / Node:
bun build --compile --target=bun --outfile my-server ./server.ts
# 得到一个 my-server 二进制,直接 ./my-server 即可运行
这对 Serverless、容器镜像瘦身、CLI 分发极其友好——镜像里只要 COPY 这个二进制,连 bun 运行时都不用装。一个典型镜像从 node:slim 的几百 MB 缩到几十 MB。
五、性能优化:让 Bun 跑满
5.1 启动时间对比(官方基准,仅供参考)
Bun 的杀手锏是冷启动。官方基准里:
- 执行一个简单脚本:Bun 比 Node 快约 4 倍;
- 跑测试套件:因并行 + 无转译开销,
bun test常常比jest快一个量级; - 装依赖:
bun install比npm快 25~30 倍。
注意:"官方基准"是理想场景。真实业务里,瓶颈常在数据库、网络、算法,而非运行时。别指望换 Bun 就能让慢 SQL 变快。
5.2 生产服务调优清单
- 热路径避免大 JSON:
Response.json()会在内存里序列化整个对象。超大响应改用流式ReadableStream。 - 用连接池:
Bun.SQL设connectionLimit,别每次请求 new 一个连接;Bun.redis复用客户端实例。 - 静态资源交给 Bun.file:不要
fs.readFile读进 Buffer 再塞 Response,Bun.file 走零拷贝。 --smol模式降内存:对内存敏感的场景(Serverless),用bun --smol把峰值内存压到最低,代价是 GC 更频繁。- preload 共享状态:测试/服务用
bunfig.toml的preload预加载通用模块(如 db 连接),避免每个 worker 重复初始化。 - 用 WAL 模式跑 SQLite:
PRAGMA journal_mode=WAL让读写不互相阻塞。 - 复用对象 / 避免热循环里分配:和任何 JS 引擎一样,减少 GC 压力 = 更稳的 P99。
5.3 一个流式响应的实战
Bun.serve({
port: 3000,
fetch(req) {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 1_000_000; i++) {
controller.enqueue(new TextEncoder().encode(`line ${i}\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: { "content-type": "text/plain; charset=utf-8" },
});
},
});
百万行数据边生成边发送,内存占用恒定,不会因为攒 full body 而 OOM。
5.4 怎么科学测性能(方法论)
别只信官方数字,自己量:
# 冷启动对比(各跑 10 次取中位数)
hyperfine --warmup 3 'bun hi.ts' 'node hi.js'
# HTTP 吞吐(用 oha / wrk / autocannon)
bun x autocannon -c 100 -d 10 http://localhost:3000/health
测性能的三个铁律:用真实数据量和真实依赖、看 P99 而非平均值、在目标部署环境(而非开发机)测。开发机上快 4 倍,到了 K8s 小规格Pod 上可能因为 CPU 限额而收益打折。
5.5 bunfig.toml:把约定固化下来
[install]
exact = true
fund = false
[test]
preload = ["./test/setup.ts"]
coverage = false
[serve]
port = 3000
把常用配置写进 bunfig.toml,团队就不用每人记一堆命令行参数。
六、常见排障清单(实战踩坑)
即便 Bun 兼容度很高,真实项目迁移仍会撞到几类坑:
- 类型不报错但运行崩:Bun 转译不做类型检查。CI 里务必加
bun x tsc --noEmit兜底,否则类型错误会流到线上。 - 某个 npm 包装不上:少数包依赖特定 V8 内部或 Node 私有 API。先
bun pm trust看是否沙箱拦截,再查该包是否用process.binding之类黑魔法。 - 原生 addon 加载失败:N-API addon 大多能跑,但依赖
node-gyp编译、或写死.node路径的包可能失败。优先考虑纯 JS 替代或等 Bun 兼容更新。 bun install后行为异常:检查bun.lockb与package-lock.json是否混用;建议迁移时删掉旧 lock 文件,统一用bun.lockb。- 内存比预期高:没开
--smol且长连接多时,--smol通常能省 30%~50% 峰值内存。 - WebSocket 收不到消息:确认客户端发的确实是
websocketupgrade,且server.upgrade返回 true;undefined返回是正确的(表示已接管)。
七、生态与框架:Bun 上能写什么
- Web 框架:Elysia(为 Bun 深度优化,类型安全路由)、Hono(超轻量、跨运行时)都原生支持 Bun.serve,性能表现亮眼。
- ORM:Drizzle、Prisma 都支持 Bun 驱动;Drizzle + Bun.SQL 组合在类型安全和速度上都很能打。
- 全栈:Bun + React Server Components / 前端 bundler 形成完整链路,
bun build直接出生产包。 - CLI / 工具:
bun build --compile让用 TS 写 CLI 并分发二进制变得极简,很多内部工具已经用 Bun 重写。
八、一次真实迁移复盘(实战视角)
光讲原理太空,讲一个贴近实战的迁移过程——某中型团队把一个 Node + Express + Jest 的内部 API 服务迁到 Bun。以下数字为典型区间,非精确基准,但顺序和量级符合多数团队的体感。
第一阶段:只换包管理器(零风险)。 他们做的第一件事不是动 runtime,而是把 npm install 换成 bun install,并删掉 package-lock.json、改用 bun.lockb。CI 里装依赖从 2 分 40 秒降到 11 秒,这一步没有任何代码改动,纯粹收益。结论:包管理器是 Bun 性价比最高、风险最低的切入点,几乎所有团队都应该先做这步。
第二阶段:换测试运行器(低风险)。 把 Jest 配置平移到 bun test。绝大多数 expect 断言和 mock 直接复用,只有两处依赖 jest.mock 的模块工厂语法需要调整成 bun:test 的 mock;一个用了 jsdom 的组件测试换成 happy-dom 后跑通。单测套件从 48 秒降到 6 秒,开发体验立竿见影地变好。注意:不要把类型检查寄托给 bun test——他们额外在 CI 加了 bun x tsc --noEmit,补上 Bun 转译跳过的类型门禁。
第三阶段:新接口用 Bun.serve 试水(中风险)。 团队规定新写的 HTTP 接口走 Bun.serve + Elysia,老接口暂留 Express。压测显示同等规格 Pod 下,Bun 服务的 P99 延迟更低、冷启动更快,但他们在生产灰度时撞到一个坑:Express 中间件里一处依赖 process.nextTick 的微任务顺序,在 JSC 的事件循环调度下表现不同,导致偶发竞态。排查手段是把该段逻辑改成显式 await,问题消失。教训:迁移 runtime 层最大的不确定性来自引擎事件循环和微任务时序的差异,这类问题不会在单测暴露,必须靠真实流量灰度。
第四阶段:全量切换的取舍(高风险,按需)。 他们最终没有全量切——因为核心计费模块依赖一个深度 hook Node http 的私有行为的老 SDK,在 Bun 上验证成本过高。于是保留 Node 跑那一个服务,其余迁移 Bun。这个克制反而让迁移三个月内平稳落地,没有引发线上事故。
复盘结论:Bun 迁移的收益曲线是「先陡后平」的——包管理和测试两步就能拿到 80% 的收益,runtime 切换的边际收益要高得多却也险得多。聪明的做法是吃到前两步红利,把 runtime 切换当作可选优化而非政治任务。
九、总结与展望
Bun 在 2026 年已经不是"玩具"。它稳定支持绝大多数 Node API,主流框架原生适配,Vite 生态也在打通。我的判断:
适合上 Bun 的场景:
- 新项目、CLI 工具、Serverless 函数;
- 对冷启动和依赖安装速度敏感的服务;
- 想要"一个二进制搞定工具链"的小团队。
暂时谨慎的场景:
- 重度依赖 Node 原生插件(N-API/C++ addon)的老项目,兼容性要逐一验证;
- 对运行时长期稳定性要求极高、不敢追新版本的金融核心系统;
- 团队完全没有 Zig/底层排障能力的,遇到问题可能踩坑较深。
迁移建议(渐进式):
- 先用
bun install替换 npm,立刻吃到装包提速; - 再用
bun test替换 Jest,CI 时间腰斩,并补tsc --noEmit类型门禁; - 新写的 HTTP 服务用
Bun.serve试水,拿真实流量对比 Node; - 最后才考虑全量切 runtime,且保留回退到 Node 的能力。
2026 年的 JavaScript 工具链,正在从"一堆 npm 包拼起来"走向"一个二进制全包圆"。Bun 是这条路上的代表,但不一定是终点——Deno 2、Node 自身的原生 TS 支持也在追赶。作为工程师,我们关心的从来不是哪个运行时"最酷",而是哪套组合能让代码跑得更快、部署更轻、排障更省心。Bun 至少在这三点上,已经给出了让人无法忽视的答案。