编程 Bun 深度拆解:当「一个二进制」终结 Node.js 工具链碎片化——从 JavaScriptCore 事件循环、Node 兼容层到 11 天 Zig→Rust 重写的全链路实战

2026-08-19 01:42:38 +0800 CST views 8

Bun 深度拆解:当「一个二进制」终结 Node.js 工具链碎片化——从 JavaScriptCore 事件循环、Node 兼容层到 11 天 Zig→Rust 重写的全链路实战

2026 年 7 月 8 日,Bun 作者 Jarred Sumner 发了一篇让整个前端圈炸锅的博客:他用 64 个 Claude 实例并行跑了 11 天,把 Bun 从 Zig 完全重写成了 Rust,写了超过 100 万行代码,烧掉约 16.5 万美元的 API 费用,换来的是 v1.4.0 Canary 修复 128 个 bug、运行速度再提升 2%~5%。

这不是又一篇「XX 吊打 Node」的营销稿。Bun 从 2022 年诞生起,就带着一个极其锋利的工程命题:能不能用一个二进制文件,把运行时、打包器、转译器、包管理器、测试运行器全部吃掉? 本文从引擎选型、事件循环与 GC 的婚姻、Node 兼容层这个最脏也最值钱的活、包管理与打包器的性能密码,一直聊到这场 AI 辅助巨型重写的工程复盘,并配可运行代码与生产级优化清单。


一、背景介绍:我们为什么还需要又一个 JavaScript 运行时

Node.js 的伟大毋庸赘述。它把 JavaScript 从浏览器里拽出来,让前端工程师第一次拥有了完整的服务端能力。但 2026 年的今天,任何维护过中大型 Node 项目的工程师都心知肚明:工具链的「熵」已经高到离谱。

一个典型的现代前端/全栈项目,开发依赖里至少躺着这些东西:

  • 运行时:Node.js(V8)
  • 包管理器:npm / yarn / pnpm,三选一,配置文件各写一套
  • 转译器:Babel / swc / tsc,TypeScript 和 JSX 离不开它
  • 打包器:webpack / rollup / esbuild / vite,按场景切换
  • 测试运行器:Jest / Vitest / Mocha,配置又是一套
  • 代码检查:ESLint / Prettier
  • 任务运行器:npm scripts / make / turborepo

问题是,这些工具各自独立演进、各自有配置、各自启动一次 Node 进程。启动一个测试,Jest 要先拉起 V8、加载 babel-jest 变换器、构建模块图;跑一次 tsc,又是一个独立的类型检查进程;webpack 打包时再拉起一遍。每一次「工具启动」都是一次 V8 冷启动 + 依赖解析,累积起来的时间成本,在 CI 和本地开发里被反复放大。

Bun 的创始人 Jarred Sumner(前 Stripe 工程师)当时思考的核心问题就是:这些活能不能在同一个进程、同一套 AST、同一个二进制里干完? 于是 Bun 的设计哲学从第一天起就是「All-in-One」——一个 bun 可执行文件,既是运行时,也是 bun install、是 bun run、是 bun test、是 bun build、是 bunx、是 bun bun 热重载。

而 2025 年 12 月,Anthropic 完成了它的首笔公开收购,把 Bun 团队收入囊中——这也为 2026 年 7 月那场「11 天 Zig→Rust 重写」埋下了伏笔(后面专门复盘)。


二、核心概念:Bun 到底「All-in-One」了什么

先把 Bun 的五个身份摆清楚,这是理解后续架构的前提:

身份命令替代对象
运行时(Runtime)bun run app.tsNode.js
包管理器(Package Manager)bun installnpm / yarn / pnpm
打包器(Bundler)bun buildwebpack / esbuild / vite
测试运行器(Test Runner)bun testJest / Vitest
任务运行器(Task Runner)bun run <script>npm scripts

它原生支持 TypeScript、JSX、TSX,无需任何前置构建步骤就能直接 bun run index.tsx。关键在于:Bun 不是「先调用 tsc 转译再交给 Node」,而是自己在内部用一套转译管线把 TS/JSX 编译成字节码后直接喂给引擎

2.1 为什么是 JavaScriptCore,而不是 V8?

这是 Bun 和 Deno 最根本的分叉点。Deno 和 Node 都用 V8(Chrome 的引擎),Bun 却选了 JavaScriptCore(JSC,Safari 的引擎)

选 JSC 的核心工程考量有三个:

  1. 启动速度快。JSC 的字节码缓存(Bytecode Cache)机制成熟,冷启动到执行第一行业务代码的时间显著短于 V8。对一个「连跑个测试都要拉起进程」的工具链来说,启动延迟是乘数级的成本。
  2. 内存占用低。JSC 的脏 slot 标记、延迟回收等设计,在长尾空闲场景下内存更克制。
  3. 嵌入友好。JSC 的 C API 相对干净,Bun 用 Zig(后来是 Rust)写宿主层去驱动 JSC,比驱动 V8 的隔离槽(Isolate)/上下文(Context)模型要顺手。

代价是:JSC 的怪癖和坑比 V8 多,且社区围绕 V8 的优化经验无法直接迁移——这意味着 Bun 团队在兼容性和引擎调优上要自己趟更多雷。这是后来 Bun 1.3 把「JSC 的 GC 与事件循环集成」当作头号优化来做的技术背景。

2.2 Zig 的历史角色与 2026 的 Rust 转向

早期 Bun 用 Zig 写宿主层和系统调用层。Zig 没有隐藏的内存分配、没有运行时、手动管理一切,非常适合写需要榨干性能、贴近内核的运行时底层。但 Zig 的代价也 brutal:内存错误和崩溃更难根治,编译器本身还在快速演进,工具链不稳定。

这正是 2026 年 7 月那场重写的直接动机——Sumner 公开表示,Zig 经常性的内存错误和崩溃「很难彻底修复」,而 Rust 能在编译期捕获大量这类错误,提高可靠性。


三、架构分析

3.1 JavaScriptCore 与事件循环的「婚姻」(Bun 1.3 的头号优化)

很多人不知道的一件事:Bun 1.3 之所以被称为「史上最大版本」,性能跃迁的关键不是某个新 API,而是 把 JavaScriptCore 的垃圾回收器(GC)和 Bun 自己的事件循环真正「结了婚」

在传统的嵌入式做法里,JSC 跑在自己的循环里,事件循环(处理网络 IO、定时器、文件 IO)跑在另一套机制里,两者通过某种桥接反复「跳出/进入」。这种割裂会导致:哪怕你的服务完全空闲,CPU 和内存也不肯真正躺平——因为两套循环各自保留了自己的调度开销与缓冲。

Bun 1.3 把 JSC 的 GC 直接挂到 Bun 的事件循环上后,效果被 Bun 工程师 Dylan Conway 描述为:空闲 CPU 时间减少约 100 倍,空闲内存占用减少约 40%。这意味着一个用 Bun 写的 HTTP 服务,在没流量的时候,几乎不占资源——这对 serverless、边缘函数、常驻守护进程是质变。

工程启示:选引擎不只是选「谁跑得快」,更是选「谁能在空闲时真正安静下来」。前端的性能焦虑大多盯着峰值吞吐,但后端常驻服务的成本大头往往是空闲态。

3.2 网络层:站在 uWebSockets 的肩膀上

Bun 的 HTTP 服务器不是从零手搓的 TCP 栈,而是构建在 uWebSockets(uWS)之上——这个 C++ 库以极致的吞吐和低延迟著称,也是许多高性能 Node 框架(如 uWebSockets.js)的底座。Bun 用宿主语言(原 Zig,现 Rust)封装 uWS,并向 JS 层暴露一个非常简洁的 Bun.serve 接口。

这就是为什么 Bun.serve 写起来像玩具、压测起来却很能打:重活都在底层 C++ 里用 epoll/io_uring 级别的 IO 多路复用干完了,JS 回调只负责业务逻辑。

3.3 Node 兼容层:最脏、也最值钱的活

Bun 能不能活下来,不取决于它多快,而取决于 能不能跑别人的代码。Node 生态有海量的 node: 内置模块(fspathhttpcryptostream……)、CommonJS 与 ESM 的互操作、还有千奇百怪的原生插件(N-API / node-gyp)。

Bun 的兼容层做了三件事:

  1. node: 模块 shim:用 Bun 自己的实现去对齐 Node 的 API 行为。这部分最痛苦——不是签名对齐就行,而是边界行为(错误类型、错误码、流式背压、事件时序)都要尽量一致,否则 require('some-old-lib') 就炸。
  2. CJS/ESM 互操作:Bun 默认把 .js 当 ESM(可在 package.json 里用 "type" 控制),但大量老库是 CommonJS。Bun 在内部做模块格式的动态识别与桥接,使得 import 老 CJS 包基本无感。
  3. 原生插件:通过 N-API 兼容层,让用 C/C++/Rust(napi-rs)写好的原生模块能在 Bun 里加载。

这一层是 Bun 工程量的「隐形大山」。它不像「启动快 4 倍」那样能写进标题,却是用户 bun install && bun run 之后不报错的根本保障。Bun 1.3 在这一块做了大量补齐,也是用户感知最明显的「终于能跑我的项目了」的版本。

3.4 包管理:bun install 为什么能比 npm 快一个数量级

bun install 官方宣称比 npm 快约 25~28 倍,不是吹的,底层有几个硬杠杆:

  • 并行解析与下载:依赖树解析、metadata 获取、tarball 下载全面并行,而不是 npm 早期那种串行/半串行。
  • 全局缓存 + 内容寻址:下载过的包按内容哈希存进全局缓存,换项目不重复下载;bun.lockb 是二进制锁文件,解析比 package-lock.json 的文本解析快得多。
  • 隔离安装(Isolated Installs):每个包被安装到 node_modules 下只属于它自己的子目录,只能访问它被声明依赖的包,从机制上杜绝了「幽灵依赖」(phantom dependency,即用了没声明的包却没报错)和依赖版本错配。
  • 内置 .gitignore.npmrc 等约定:减少配置心智负担。
# 传统三步,在 Bun 里合并成零配置一步
npm install        # -> bun install
npm run dev        # -> bun run dev
npx tsc            # -> bunx tsc  (bunx 自带包解析与缓存)

3.5 打包器:从零 Parser 到 Tree-shaking

bun build 不是调 esbuild 的壳,而是 Bun 自己用宿主语言实现的 parser + transformer + linker:

  • 原生解析 TS/JSX/TSX/CSS/HTML,不依赖 Babel,所以转译极快;
  • 支持 tree-shaking(基于 ES Module 的静态分析剔除死代码)、code splitting(按动态 import() 拆包)、CSS/HTML 打包
  • 关键杀手锏:bun build --compile 能把整个应用 + 运行时打包成一个独立的单文件可执行程序,对方机器上不需要装 Node,也不需要装 Bun。
# 把后端服务编译成单文件二进制,直接scp到服务器运行
bun build ./src/server.ts --compile --outfile=myapp
./myapp   # 不需要 node,不需要 node_modules

这对交付内部工具、CLI、边缘部署是降维打击。

3.6 复盘:11 天、100 万行、16.5 万美元——Zig→Rust 重写

回到开头那颗炸弹。2026 年 7 月 8 日,Sumner 宣布:在 Claude(Fable 5)的辅助下,用 64 个 Claude 实例并行跑了 11 天,把 Bun 从 Zig 完全重写为 Rust,产出超 100 万行代码

几个值得工程师咀嚼的点:

  • 动机是可靠性,不是性能。他说 Zig 的内存错误/崩溃「很难彻底修复」,Rust 的借用检查器能在编译期挡掉一大类。对一个被 Anthropic 收购、要承载大量生产的运行时,稳定性优先于极致手动优化。
  • 代价可观但可承受:约 16.5 万美元的 API 费用,因为 Bun 已被 Anthropic 收购,团队不必自己掏腰包。换算成人力,Sumner 估计若纯人工团队重构需要 1 年
  • 产出是增量交付:重写不是「停工半年憋大招」,而是以 v1.4.0 Canary 形式发布,修复 128 个 bug,运行速度反而再提升 2%~5%——说明 Rust 版在编译器护航下,连隐含的内存/并发问题都顺手清掉了。

客观提醒:如此体量的 AI 辅助重写,真实工程里最大的风险不是「写不出来」,而是「跑起来语义对不对」。Bun 能直接以 Canary 落地,依赖的是它庞大的测试矩阵 + Node 兼容测试套件——没有测试护城河,这种重写就是灾难。这给所有想用 AI 做大型重构的团队上了一课:让 AI 写 100 万行容易,让你的测试能验证这 100 万行才难。


四、代码实战

下面所有示例均可直接 bun run 运行(Bun 原生支持 TS/JSX,无需额外配置)。

4.1 一行起一个 HTTP 服务

// server.ts
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/api/hello") {
      return Response.json({ message: "hello from Bun", ts: Date.now() });
    }

    if (url.pathname === "/echo" && request.method === "POST") {
      const body = await request.json();
      return Response.json({ youSent: body });
    }

    return new Response("Not Found", { status: 404 });
  },
});

console.log(`🚀 listening on http://localhost:${server.port}`);

注意这里用的是标准 Web API Request/Response/fetch和浏览器里写的一模一样——Bun 刻意对齐 Web 标准,而不是 Node 的 req.on('data') 回调风格。这也是它「前端友好」的来源。

4.2 文件与流:Bun.file / Bun.write

// 读文件
const pkg = Bun.file("./package.json");
const text = await pkg.text();
console.log("package.json bytes:", pkg.size);

// 写文件(支持 string / Uint8Array / Blob / 另一个 BunFile / 流)
await Bun.write("./dist/output.txt", "hello bun");

// 把远程内容流式落地到本地,零拷贝式管道
const remote = await fetch("https://example.com/big.bin");
await Bun.write("./big.bin", remote.body!);

Bun.file 返回的是 Blob 的超集,天然支持 text() / json() / arrayBuffer() / stream(),还带 sizetype 等元信息。

4.3 SQLite 与统一 SQL API

Bun 内置了 bun:sqlite,无需 npm install sqlite3 就能用,且是同步零拷贝 API:

import { Database } from "bun:sqlite";

const db = new Database(":memory:");
db.run("CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
db.run("INSERT INTO user (name, age) VALUES (?, ?)", ["Alice", 30]);

const users = db.query("SELECT * FROM user WHERE age > ?").all(18);
console.log(users);

// 事务:用函数包裹,自动 commit/rollback
db.transaction(() => {
  db.run("INSERT INTO user (name, age) VALUES (?, ?)", ["Bob", 25]);
})();

更炸裂的是 Bun 1.3 引入的统一数据库 API Bun.sql,用同一套 tagged template 语法就能操作 PostgreSQL / MySQL / SQLite:

import { sql } from "bun";

// 连接串走环境变量 BUN_DB_URL(postgres:// / mysql:// / sqlite://)
const rows = await sql`SELECT id, name FROM user WHERE age > ${18}`;
console.log(rows);

// 跨数据库也能复用同一套写法,迁移数据库几乎不改业务代码

4.4 测试:bun test

bun test 兼容 Jest 风格的 expect / describe / test,且自带 DOM、mock、snapshot、coverage,启动只要毫秒级:

// math.test.ts
import { expect, test } from "bun:test";

function add(a: number, b: number) {
  return a + b;
}

test("add works", () => {
  expect(add(1, 2)).toBe(3);
});

test("add is commutative", () => {
  expect(add(2, 3)).toBe(add(3, 2));
});
bun test          # 跑全部
bun test math     # 按文件名过滤
bun test --coverage

4.5 编译成单文件可执行程序

bun build ./src/cli.ts --compile --outfile=mycli
./mycli --help    # 对方机器无需 Node / Bun

配合 Bun.serve 的常驻服务,这套组合能让你把「源码 → 交付物」的链路压缩到一条命令。

4.6 实用小工具:Transpiler / Glob / 哈希

import { Glob } from "bun";

// 遍历项目里所有 ts 文件,比手搓 fs 递归清爽得多
for (const file of new Glob("**/*.ts").scan(".")) {
  console.log(file);
}

// 在 JS 里直接转译代码(复用 Bun 的 parser)
const transpiler = new Bun.Transpiler({ loader: "tsx" });
const js = transpiler.transformSync(`const x: number = 1; console.log(<h1>{x}</h1>);`);
console.log(js);

// 高性能哈希 & 纳秒计时(底层直接用宿主层实现)
console.log(Bun.hash("hello"));        // 稳定 32 位哈希
console.log(Bun.nanoseconds());        // 高精度计时,做 benchmark 神器

4.7 从 Node 迁移:package.json 实战

Bun 尽量零配置,但你可以在 package.json 里用 "bun" 字段集中声明 Bun 专属行为:

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "dev": "bun run --hot src/server.ts",
    "build": "bun build ./src/server.ts --compile --outfile=dist/app",
    "test": "bun test"
  },
  "bun": {
    "install": {
      "exact": true,
      "frozenLockfile": true
    },
    "test": {
      "coverage": true
    }
  }
}

bun run --hot 提供文件级热重载,改代码秒级生效,对标 nodemon 但内置于运行时。


五、性能优化:生产级清单

把 Bun 用进生产,别只盯着「启动快」。按优先级给你一份实战清单:

  1. 静态资源交给 Bun.serveserveStatic / 反向代理:Bun 的 HTTP 吞吐已经很强,但 TLS 卸载、gzip、缓存头建议放到前面的 Nginx / CDN,Bun 专注业务逻辑。
  2. 利用空闲态优势做常驻服务:既然 1.3 之后空闲 CPU/内存极低,把一批小工具合并成常驻守护进程(而非每次 cron 拉起 Node)能省大量冷启动。
  3. 数据库走 Bun.sql 连接池Bun.sql 自带连接管理,避免在每个请求里新建连接;PostgreSQL/MySQL 记得配置连接数上限。
  4. bun build --compile 做边缘/CLI 交付:无需运行时依赖,缩小攻击面、加快冷启动。
  5. Bun.nanoseconds() 做微观 benchmark,定位真正的热点,而不是凭直觉优化。
  6. 警惕原生插件:纯 JS/TS 路径最优;引入 N-API 原生模块时,确认它在 Bun 下的兼容性,避免把兼容性雷区带进生产。
  7. 锁文件策略:CI 里用 bun install --frozen-lockfile,配合隔离安装,杜绝幽灵依赖线上爆雷。

六、总结与展望

Bun 的意义,从来不只是「比 Node 快」。它真正挑战的是一种工程惯性:我们是不是真的需要为「运行、安装、打包、测试、转译」分别维护五套工具、五套配置、五次进程冷启动?

从技术纵深看,Bun 的三块硬骨头都啃得有声有色:

  • 引擎选型(JSC) 带来了启动与空闲态的双重优势;
  • 事件循环与 GC 的集成(1.3) 把空闲资源压到极低,是常驻服务的隐形红利;
  • Node 兼容层 让它真正「能跑别人的代码」,这是生死线。

而 2026 年 7 月这场 Zig→Rust 重写,则把「AI 辅助巨型重构」从 PPT 拉进了现实:64 个 Claude 实例、11 天、100 万行、修复 128 个 bug 还顺带快了 2%~5%。它证明了在大模型时代,重写一个百万行级基础设施并非不可能,但前提是你要有一张能验证正确性的测试安全网——Bun 的 Node 兼容测试套件,才是这场豪赌真正的底牌。

对开发者我的建议很直接:

  • 新项目 / 内部工具 / CLI / 边缘函数,无脑上 Bun,开发体验和交付效率是断层领先;
  • 维护重历史包袱的老 Node 项目,先 bun install 试兼容性,再逐步把 bun run / bun test / bun build 替换进去,别一上来就换运行时;
  • 关注 Bun v1.4(Rust 版) 的后续稳定:重写后的可靠性红利,大概率会在接下来的几个小版本里集中释放。

Node.js 不会死,V8 生态的积累太厚;Deno 在安全和标准上另辟蹊径;而 Bun 用「一个二进制终结碎片化」的极简暴力,给整个 JS 工具链上了一课。多运行时共存的时代已经到来,而选择权,终于回到了开发者手里。

推荐文章

deepcopy一个Go语言的深拷贝工具库
2024-11-18 18:17:40 +0800 CST
程序员茄子在线接单