Bun vs Node.js:深度对比与底层架构解析
前言
2025年,随着 Anthropic 收购 Bun 并将其打造为 AI Agent 的首选运行环境,JavaScript 运行时之战进入了全新阶段。本文基于一次深度的技术对话,系统性地拆解了 Bun 的技术本质、Node.js 的构成、以及 Linux 高性能 I/O 接口的演进历程。gemini.google.com/app/351f377…
一、Bun 是什么?为什么它值得关注?
Bun 是一个高性能、一体化的 JavaScript/TypeScript 运行时和工具包,由 Jarred Sumner 于 2021 年首次发布。它不仅仅替代 Node.js(作为运行时),还内置了:
- 包管理器(替代 npm/yarn/pnpm)
- 打包器(替代 Webpack/Vite/esbuild)
- 测试运行器(替代 Jest/Vitest)
- 内置 SQLite(
bun:sqlite)
1.1 技术栈解析
| 维度 | Node.js | Bun |
|---|---|---|
| 底层引擎 | V8 (Google) | JavaScriptCore (Apple) |
| 核心语言 | C++ | Zig |
| 启动速度 | 较慢(V8 优化长期运行) | 极快(JSC 优化冷启动) |
| TS 支持 | 需要外部转译(tsc/tsx/ts-node) | 原生支持(直接运行 .ts/.tsx) |
| 模块系统 | 区分 CJS/ESM 较麻烦 | 无缝兼容(同一个文件可混用 import 和 require) |
| 生态兼容性 | 行业标准,100% 兼容 | 约 95%+ 兼容,极少数底层 C++ Addon 不支持 |
1.2 为什么选择 JavaScriptCore 而不是 V8?
这个问题触及了高性能运行时的核心取舍:
-
启动速度:V8 设计初衷是为长期运行的 Chrome 浏览器优化,有复杂的 JIT 预热过程。JSC 设计初衷是为移动端 Safari 优化,启动极快,适合 Serverless/CLI 场景。
-
内存占用:JSC 在内存分配上比 V8 更加节俭,更少的内存意味着更低的容器化部署成本。
-
架构可塑性:JSC 的 C API 相对更干净,Bun 作者可以用 Zig 编写更高效的"桥接代码",让 JS 变量和 Zig 内存之间的交换几乎零开销。
二、Bun 如何实现 CJS/ESM 无缝混用?
这是 Bun 最令人惊叹的特性之一,也是最容易引发误解的地方。
2.1 核心原理:编译时"实时转译"
Bun 在执行代码之前,会通过其高度优化的 Zig 编写的转译器对源码进行扫描:
- 预转译 (Transpilation):将 ESM 模块实时转换为类似 CJS 的格式
- 统一为 Binding 模式:在 JS 引擎内部,所有模块(包括 CJS)都表现得像 ESM,建立引用关系而非值拷贝
2.2 "转译为 CJS"与"表现得像 ESM"矛盾吗?
不矛盾,这是"手段"与"目标"的关系:
- 手段:为了能让
require()同步调用 ESM,Bun 必须在语法层面把export default变成类似module.exports的形式 - 目标:在 JSC 引擎内部,Bun 的
require返回的是一个实时代理(Live Binding),而非快照值
举例说明:假设模块 A 导出一个变量 count,内部有定时器在增加它:
- Node.js (CJS):
require('./A')拿到的是那一刻的值,即使 A 内部加到了 100,你手里的变量还是 0 - Bun:无论用
import还是require,拿到的都是指向 A 模块内存地址的引用,当 A 内部变到 100,你读取到的也是 100
2.3 Tree Shaking 如何处理?
由于 Bun 本身就是打包器,Tree Shaking 是在 Zig 编写的转译阶段同步完成的:
- 静态分析:Zig 以极快的速度扫描整个依赖图
- 死代码删除 (DCE):基于"类 ESM"的引用模式,能清晰追踪到哪些导出从未被引用
- 对 CJS 的"暴力"优化:常量折叠 + 属性追踪,即使是被导出为大对象的 CJS 包(如 lodash),如果只访问了
_.cloneDeep,其他属性也会被剔除
三、Bun 的架构:为什么不需要 libuv?
3.1 事件循环的本质
所有异步 JS 运行时都必须有事件循环。Bun 的事件循环不是"没有",而是用 Zig 直接对接系统内核实现。
Node.js 的逻辑(套娃模式):
JS 代码 -> V8 引擎 -> Node C++ 绑定 -> libuv -> 系统内核调用(epoll/kqueue/IOCP)
Bun 的逻辑(直连模式):
JS 代码 -> JSC 引擎 -> Zig 编写的内核驱动 -> 系统内核调用
3.2 libuv 的必要性还在吗?
libuv 的存在主要是为了跨平台一致性。它为 Node.js 解决了:
- 网络 I/O:Linux 用 epoll、macOS 用 kqueue、Windows 用 IOCP
- 文件 I/O:通过线程池模拟异步(因为 Unix/Linux 长期以来缺乏完美的原生异步文件接口)
Bun 选择舍弃 libuv,意味着必须为 Windows (IOCP)、Linux (io_uring/epoll)、macOS (kqueue) 手动维护三套高度复杂的底层代码。这也是 Bun Windows 版本开发进度较慢的根本原因。
3.3 舍弃 libuv 的优势与代价
| 优势 | 代价 |
|---|---|
| 消除"抽象税",路径更短、指令更少 | 跨平台维护成本极高 |
| 零内存分配的异步 | 部分 Node C++ Addons 可能不兼容 |
| 纵向集成优化(Transpiler + Runtime 都是 Zig) | 社区验证的广度不足 |
四、epoll vs io_uring:高性能 I/O 接口演进
4.1 epoll 是什么?
epoll 是 Linux 内核中一种可扩展的 I/O 事件通知机制,允许一个线程同时监控成千上万个文件描述符。Nginx 能够支撑百万并发的核心原因就是使用 epoll。
核心组件:
- 红黑树:高效增删改查 10 万级连接,查找速度 O(logn)
- 双向链表(Ready List):内核硬件中断触发回调,将就绪的 FD 移入此链表
- 回调机制:不是轮询,而是内核事件触发
4.2 io_uring 的革命性创新
io_uring 是 Linux 5.1+(2019年)引入的高性能异步 I/O 框架,引入了双环形缓冲区 (Dual Ring Buffers) 架构:
- 提交队列 (SQ):程序往里写任务(读取文件、发送数据包)
- 完成队列 (CQ):内核处理完任务后填入结果
- 核心架构:通过 mmap 映射共享内存,双方看同一块物理地址
4.3 io_uring 的三种通知模式
| 模式 | 交互方式 | 适用场景 |
|---|---|---|
| 默认模式 | 程序调用 io_uring_enter(),内核完成后唤醒 | 通用场景 |
| 轮询模式 (IOPoll) | 程序不断查看 CQ 尾指针 | NVMe 等低延迟存储 |
| SQPOLL 模式 | 内核线程死循环盯 SQ,0 次系统调用 | 极致性能场景 |
4.4 为什么 io_uring 比 epoll 快?
- 上下文切换:epoll 每次问"谁好了"都要系统调用(用户态→内核态切换),io_uring SQPOLL 模式下可做到 0 次切换
- 数据拷贝:传统 I/O 需要内核缓冲区和用户缓冲区之间多次拷贝,io_uring 通过 mmap 让双方看同一块内存
- 吞吐能力:支持批量提交和批量完成,能压榨硬件极限
五、libuv 的架构解析
libuv 是用 C 语言编写的,核心任务有两个:磨平跨平台差异 和 管理异步事件循环。
5.1 核心构成
- 句柄 (Handles):长期占用资源的引用(TCP Server、定时器、信号量)
- 请求 (Requests):短期的特定操作(读取文件、发送数据包)
- 线程池:默认 4 个线程,用于处理无法直接利用内核异步能力的任务(文件 I/O、DNS 查询)
- 事件循环:协调所有 Handle 和 Request
5.2 事件循环的七阶段
更新时间 → 定时器阶段 → 待定回调 → 空转/准备 → 轮询阶段 → 复检阶段 → 关闭回调
5.3 libuv 的局限性
- 抽象开销:为了兼容性,在内存分配和数据传递上有"抽象税"
- 文件 I/O 瓶颈:依赖线程池处理文件操作,大量小文件读写时线程切换开销大
- 架构陈旧:设计时 io_uring 还未诞生,无法利用最先进的零拷贝、零切换特性
六、io_uring 是未来吗?
6.1 当前采用状态
| 项目 | io_uring 采用状态 |
|---|---|
| Nginx | 通过模块支持,但默认仍用 epoll,更倾向于用 io_uring 处理磁盘 I/O |
| libuv | Node.js 20+ 在 Linux 上已用 io_uring 优化部分磁盘操作,网络仍倾向 epoll |
6.2 三大阻碍因素
- 安全性问题:io_uring 早期版本被爆出多起 Linux 内核漏洞,很多公有云一度禁用了该系统调用
- 边际收益递减:对于普通 HTTP 请求,epoll 开销已经很小,在复杂业务逻辑面前不显著
- 内核版本滞后:io_uring 网络功能(如 multishot accept)直到 Linux 6.x 才真正成熟
6.3 未来判断
毫无疑问是未来。它的架构逻辑代表了操作系统的进化方向:
- 统一化接口:终结"网络用 epoll,磁盘用 libaio,信号用 signalfd"的碎片化时代
- AI 时代的加速器:大模型推理需要极致 I/O 和内存共享,io_uring 是唯一选择
- 新一代软件的基石:Bun、PostgreSQL 17+、TigerBeetle 等从第一天就围绕 io_uring 构建
七、Anthropic 收购版图解析
2025年开始,Anthropic 为补齐 AI Agent 版图进行了精准收购:
| 收购对象 | 时间 | 补充能力 |
|---|---|---|
| Bun | 2025年12月 | 底层性能 — 让 AI 生成的代码能瞬间运行和测试 |
| Vercept | 2026年2月 | 环境交互 — 攻克 Computer Use,让 AI 能操作电脑 |
| Humanloop | 2025年8月 | 反馈链路 — 企业级评估与对齐 |
一句话总结:Anthropic 正在把自己从一个"模型厂商"变成一个**"具备执行能力的 AI 操作系统"**。
八、总结:如何选择?
8.1 前端领域:必试 Bun
bun install比 npm 快 10-30 倍- 内置 TypeScript 原生支持,无需配置
- CLI 工具和脚本编写体验极佳
8.2 服务端领域:视场景而定
| 场景 | 推荐 |
|---|---|
| 内部工具/小工具 | Bun ⭐⭐⭐⭐⭐ |
| Serverless/边缘计算 | Bun ⭐⭐⭐⭐⭐ |
| 高并发网关 | Bun ⭐⭐⭐⭐ |
| 金融级核心业务 | Node.js ⭐⭐⭐⭐⭐ |
8.3 本质总结
- Node.js:社区驱动的组装机,灵活性高但有性能天花板
- Bun:一体化锻造的利刃,用 Zig + JSC 组合拳打破性能极限
- io_uring:Linux I/O 的未来,共享内存架构终结"上下文切换"噩梦
- libuv:工业级稳定性的代表,正在缓慢但坚定地拥抱 io_uring
本文基于 2026 年 3 月的技术对话整理,部分信息可能随软件版本更新而变化。