引言:一条 PR 让 GitHub 崩溃
2026 年 5 月 14 日,Bun 的 GitHub 仓库 PR #30412 页面直接被干爆了——不是因为攻击,而是因为一个 PR 包含了 6755 个 commit、2188 个文件变更、新增超过 100 万行代码。这个 PR 的标题很直接:"Rewrite Bun in Rust"。
这不是一次普通的技术更新。这是一个拥有 9 万多 star、每周数十万次下载量的 JavaScript runtime,将自己的核心代码从 Zig 全部迁移到了 Rust。而且主导这次迁移的,很大程度上不是人类工程师——是 Claude。
更荒诞的是:Anthropic 在 2025 年 12 月收购了 Bun,而 Claude Code 本身依赖 Bun 作为运行时。2026 年初,Claude Code 社区开始疯狂抱怨内存泄漏和稳定性问题。后来大家才发现,相当一部分问题根因在 Bun。最后,Anthropic 让 Claude 去重写了 Bun,然后 Bun 继续支撑 Claude Code——一个完美的自循环。
本文从工程视角深入剖析这次迁移:它是怎么做到的?技术细节是什么?社区为什么炸锅?这对 AI 时代的软件开发意味着什么?
1. 背景:Bun 与 Zig 的四年情缘为何终结
Bun 最早于 2022 年发布,定位是 Node.js 和 Deno 的"全家桶替代品"——一个二进制文件,同时是 JavaScript runtime、包管理器、测试工具和构建工具。它从零开始,用 Zig 语言编写,性能标榜比 Node.js 快数倍。Bun 选择 Zig 有其逻辑:Zig 的 comptime(编译时计算)、手动内存管理和零成本抽象,与 Bun 追求极致性能的目标高度契合。
但到了 2026 年,问题开始显现。
内存泄漏成为老大难。 2026 年 3 月,一个编号 #33453 的 Issue 被提交到 Bun 仓库:
"Claude Code 的主进程在约 3 小时的短会话中,RSS 内存从约 1.7GB 增长到 14GB 以上。泄漏位于 Bun 运行时的 WebKit Malloc 分配器中,而非用户空间的 JavaScript 分配。"
另一个 Issue #11377 记录更夸张:运行 14 小时后,Claude Code 进程占用 23GB 虚拟内存,143.8% CPU,系统完全卡死。这些问题的直接受益者(或者说受害者)正是 Claude Code——因为 Claude Code 就是以 Bun 可执行文件形式发布的,深度嵌入不可分割。
Issue 堆积如山。 Rewardo 公司 CTO Wojciech Maj 曾做过一个对比:Node.js 作为驱动全球互联网的运行时,约有 1700 个 open issues;而用户规模远小于 Node.js 的 Bun,却积累了约 4700 个 open issues。
Anthropic 收购改变了优先级。 2025 年 12 月 Anthropic 收购 Bun 后,官方说法是"加速 Claude Code 能力"。本质上,Bun 要成为 Claude Code 背后的基础设施。但 Claude Code 对 Bun 的依赖程度(以及 Bun 的内存问题严重程度)让 Anthropic 无法坐视不管。
2. 迁移过程:6 天、96 万行 Rust 代码
2.1 PORTING.md:AI 参与大规模迁移的方法论
这次迁移的真正看点,不在于"换语言",而在于如何用 AI 参与一次百万行级别的跨语言迁移。
迁移的起点是一份 576 行的 PORTING.md 文档——它不是普通的技术文档,而是一份完整的迁移流水线规范:
Phase A: 翻译阶段
- 将每个 Zig 文件翻译为同目录下的 Rust 草稿
- 目标:忠实捕捉 Zig 逻辑,不要求立即编译
- 禁止引入 tokio/rayon/hyper/futures
- 禁止使用 async fn
- 所有 unsafe 必须写明 SAFETY 注释
- 遇到不确定逻辑:宁可留下 TODO,也不要自行猜测
Phase B: 解决阶段
- 按 crate 解决编译错误
- 解决所有权和生命周期问题
- 处理平台差异(Linux/macOS/Windows)
- 性能调优
这份规范的核心洞察是:AI 在迁移中的角色是执行者,而不是决策者。 架构边界、禁止规则、类型映射策略,都是人提前写好的。AI 的任务是按流水线高吞吐地生成可审查的草稿。
同时,仓库里出现了 .claude/workflows 目录,里面定义了多个自动化工作流,对应迁移的不同阶段。
2.2 从 4000 次 commit 到 99.8% 测试通过
迁移的实际进度令人瞠目。
5 月 7 日:Jarred 发推称,Rust 迁移已涉及约 4000 次 commit、96 万行代码。当时只剩下 3 个编译错误。Rust 版本已经能显示 help menu,bun run 和 package.json scripts 也能跑起来——意味着 JSON parser、AST、logger、module resolver、文件系统遍历等一整串基础能力都已被迁移过去。
5 月 9 日:进度跳到新量级。Rust 版本在 Linux x64 glibc 环境下通过了 Bun 既有测试套件的 99.8%。
5 月 11 日:Jarred 发出一条后来引爆整个社区的推文:
"如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
5 月 14 日:PR #30412 被合并进 main。官方数据:
| 指标 | 数值 |
|---|---|
| Commit 数 | 6755 个 |
| 文件变更 | 2188 个 |
| 新增代码 | 100 万行以上 |
| 二进制体积变化 | 缩小 3-8 MB |
| 测试套件 | 通过(所有平台) |
| 修复问题 | 内存泄漏 + flaky tests |
3. 技术细节:unsafe、内存管理与性能
3.1 13,365 个 unsafe 块:真相是什么
5 月 21 日,Bun 官方发布了一份 unsafe 审计页面,其中确认了一个被社区广泛引用的数字:13,365 个 unsafe 代码块。
这个数字听起来吓人,但需要理解其来源和性质:
// 1. FFI 边界:从 JavaScriptCore、libuv、mimalloc、uWebSockets 等 C/C++ 库调用
extern "C" {
fn bun_malloc(size: size_t) -> *mut c_void;
fn js_evaluate(ctx: *mut JSContext, script: *const c_char) -> JSValue;
}
// 2. 从 Zig 移植过来的 ownership idiom(直接映射而非 Rust 原生写法)
unsafe impl Send for BunThreadPool {}
unsafe impl Sync for BunThreadPool {}
// 3. 性能关键路径:手动内存布局、SIMD intrinsics
unsafe {
let ptr = std::arch::x86_64::_mm_loadu_si128(addr as *const __m128i);
std::arch::x86_64::_mm_storeu_si128(out as *mut __m128i, result);
}
官方对这 13,365 个 unsafe 做了分类:
- 约 9,300 个:可以逐步转成 safe code,需要后续清理工作
- 约 4,000 个:保留在必要边界(FFI、SIMD、手动内存布局)
关键是:这些 unsafe 是否被清晰隔离?invariant 由谁维护?Bun 官方表示,unsafe 分布在明确的边界上,每个 unsafe 块都附有 SAFETY 注释说明其不变式。
3.2 为什么没有用 async Rust
Bun 的 Rust 版本明确选择不使用 async Rust。这是有意为之的设计决策:
// Bun 的选择:同步 + 轻量线程池
fn handle_request(req: Request, pool: &ThreadPool) -> Response {
pool.spawn_sync(|| {
// 同步执行,避免 async 调度开销
let result = blocking_query(&req);
result.into_response()
})
}
理由:
- 降低迁移复杂度:async Rust 的所有权和生命周期规则比同步 Rust 复杂得多
- 保持性能优势:Bun 的核心竞争力之一是极快的启动时间(~3ms),async 运行时引入的调度开销与这个目标冲突
- 与 Zig 版本架构对齐:原 Zig 版本不使用 async runtime,保持架构不变降低了迁移后的不确定性
3.3 Arena 分配器:高性能内存管理
Bun 的 Rust 版本采用定制化的 Arena 分配器来管理语法树和 AST 节点:
// Arena 分配器:所有 AST 节点共享一个大内存池
pub struct BumpAllocator {
buffer: Vec<u8>,
ptr: usize,
}
impl BumpAllocator {
pub fn alloc<T>(&mut self, val: T) -> &mut T {
let align = std::mem::align_of::<T>();
let size = std::mem::size_of::<T>();
self.ptr = (self.ptr + align - 1) & !(align - 1);
let out_ptr = &mut self.buffer[self.ptr] as *mut u8 as *mut T;
self.ptr += size;
unsafe { *out_ptr = val; }
unsafe { &mut *out_ptr }
}
}
// AST 节点使用 Arena 分配,避免逐个节点的 malloc/free 开销
pub struct JsAstArena {
allocator: BumpAllocator,
}
这种设计使得大量短期分配(AST 节点、字符串切片)的内存开销趋近于零。Arena 在整个解析过程结束后统一释放,不需要逐节点追踪生命周期。
4. 社区反应:从"实验"到"信任危机"
4.1 从"大概率扔掉"到"直接合并"
社区最大的不满集中在叙事变化的速度上:
- 5 月 5 日:Jarred 在 HN 上说"这只是实验,最后被全部扔掉的概率非常高"
- 5 月 7 日:4000 次 commit、96 万行代码,已剩 3 个编译错误
- 5 月 9 日:99.8% 测试通过
- 5 月 11 日:宣布将是"Zig 最后一个版本"
- 5 月 14 日:PR 合并
Reddit 用户 Xtergo 的吐槽很有代表性:
"Bun 的路线图看起来更像是在不断叠加新功能,而不是优先解决稳定性和 Bug 修复问题。Bun 现在已经变得非常复杂了。如果这些问题继续得不到解决,我怀疑它永远无法达到 Node.js 那种生产级成熟度。"
4.2 开源项目开始划清界限
yt-dlp 的立场:5 月 20 日,yt-dlp 宣布限制 Bun 作为 EJS 兼容 JavaScript runtime 的支持范围,限定到 1.2.11 至 1.3.14 版本。理由:早期 Bun 版本构建 EJS 时可能忽略 lockfile;维护者认为 Rust 重写方向有"fully vibe-coded"趋势,因此把 1.3.14(Zig 最后版本)设为上限。
Electrobun 的危机:Electrobun 架构核心就是 Bun——官方文档写着"Electrobun app 本质上是一个 Bun app"。5 月 23 日,Electrobun 面临不得不跟进的困境。
4.3 "Claude 写 Claude" 的荒诞自循环
最让社区炸锅的是一个隐喻:当 Anthropic 的 Claude Code 被 Bun 的内存泄漏坑惨后,Anthropic 让 Claude 去重写了 Bun,然后 Bun 继续支撑 Claude Code。
开发者社区的一句玩笑话精准击中了荒诞感:
"Bun 已经嵌入到 Claude Code 中。Claude Code 看起来糟透了。所以现在我担心 Bun 也可能糟透了。"
5. 深层意义:三层开发模式的雏形
5.1 三层架构
这次迁移展示了一种新兴的软件开发分工模式:
第一层:AI 负责吞吐
- 生成草稿、批量迁移、扫明显问题、跑自动验证
- 高吞吐量,24/7 不间断
第二层:工具负责约束
- 编译器检查类型和生命周期
- lint 确保代码风格一致
- 测试套件验证行为正确性
- CI/Benchmark 提前暴露问题
第三层:人负责判断
- 架构边界由人决定
- 发布节奏由人控制
- 兼容性判断由人负责
- 生产责任由人承担
5.2 "约束"成为核心资产
这次迁移带来的最重要认知转变是:在 AI 参与大型工程的时代,开发者最重要的产出不是代码,而是约束规则。
一份高质量的迁移规范,需要:
- 对原系统的架构有深刻理解
- 对目标语言的语义和惯用法有清晰认识
- 对迁移风险和不可改动的边界有明确判断
- 对验证策略和回滚预案有完整规划
这不是 prompt engineering,这是工程规范设计能力。
6. 工程教训:AI 参与大项目的风险与边界
6.1 迁移最难的是语义而非语法
跨语言迁移最大的坑从来不是语法转换,而是语义映射:
// Zig: 手动资源管理 + comptime
fn createBuffer(comptime size: usize) !*[size]u8 {
const ptr = try allocator.alloc(u8, size);
errdefer allocator.free(ptr);
return ptr[0..size];
}
// 正确翻译(需要理解 Zig 的语义)
fn create_buffer(size: usize) -> Result<Box<[u8]>> {
// Box<[u8]> 正确表达了所有权转移的语义
let ptr = vec![0u8; size].into_boxed_slice();
Ok(ptr)
}
Zig 的 errdefer、comptime、手动指针操作,在 Rust 中没有一一对应。需要理解设计意图后重新表达。
6.2 测试套件的覆盖率是幻觉
99.8% 测试通过是一个好的信号,但不是质量保证。Bun 的测试套件无法覆盖:
- 边缘平台的特定行为(Windows 的路径处理、macOS 的 ARM64 细节)
- 长期运行稳定性(内存泄漏需要数小时甚至数天才能暴露)
- 与特定第三方库交互的兼容性
- 编译器版本差异
canary 频道的价值正在这里:用真实用户和真实工作负载去探索测试套件覆盖不到的区域。
6.3 社区信任的修复比代码修复更难
从社区反应来看,Bun 面临的不只是技术问题,更是信任问题:
- 叙事透明度:从"实验"到"合并"的叙事转变太快
- 维护者负担:Rust 版本后续清理工作(9,300 个可移除的 unsafe)需要持续投入
- 向后兼容承诺:Bun 的 API 稳定性承诺在语言切换后是否仍然有效?
7. 对未来软件开发的启示
7.1 大规模迁移的成本正在下降
Bun 的案例证明:借助 AI 和系统化的工程规范,一次百万行级别的跨语言迁移可以在 6 天内完成初步可运行版本。这意味着:
- 以前不敢动的遗留系统:现在可以先让 AI 生成 70% 的草稿,人来审查和修复剩下的 30%
- 技术债务不再是永远的债务:人力不够而长期拖着的迁移问题,现在有了新的解决路径
- 快速验证比完美更重要:先跑起来,再迭代,而不是等到"准备好了"再开始
7.2 "开发者"这个角色的内涵在变化
过去评价一个开发者的能力,看的是"能写多好的代码"。未来可能更多看"能设计多好的工程系统"——包括约束规则、验证策略、回滚预案、架构边界。
7.3 AI 参与工程的前提:清晰的边界和完整的测试
Bun 迁移能相对顺利,依赖于几个前提:
- 清晰的架构边界:Bun 的模块结构相对清晰
- 完整的测试套件:99.8% 的通过率给了合并决定足够的信心
- 保守的迁移策略:保持架构和数据结构不变,只换底层实现语言
这三个前提不是 AI 能给的,是人类工程师多年积累的结果。
8. 总结
Bun 的 Rust 重写,是 2026 年软件开发领域最值得记录的技术事件之一。它用 6 天时间、100 万行新增代码、6755 个 commit,完成了一次曾经被认为需要数月甚至数年的工程迁移。
这背后的核心驱动力是 AI。但 AI 能做到的事情,取决于人类给它划定的边界。PORTING.md 定义了迁移规则,.claude/workflows 执行流水线,测试套件验证结果——人的判断、工具的约束、AI 的执行,三者缺一不可。
这次迁移也揭示了一个深层矛盾:AI 让"做一次试试"的成本大幅降低,但"做得对"的判断成本仍然很高。社区的信任不在于代码量,而在于维护者能否对结果负责。
对整个 JavaScript 生态而言,Bun 的这次尝试提供了一个有价值的参考:当一个项目发展到足够大规模时,如何在保持向后兼容的同时,为未来的稳定性投资?
用更现代的系统编程语言重构核心组件,是值得考虑的选择。但前提是:清晰的工程边界、完整的测试覆盖,以及对社区的透明沟通。
毕竟,AI 能写代码,但最终承担生产责任的仍然是人。
参考资料
- GitHub PR #30412: oven-sh/bun - Rewrite Bun in Rust
- GitHub Issue #33453: Claude Code memory leak
- Bun Unsafe Audit: 官方审计页面(github.com/oven-sh/bun/blob/main/docs/unsafe-audit.md)
- PORTING.md: Bun 仓库内的 Zig to Rust 迁移指南(576 行)
- Jarred Sumner @X (2026-05-07 至 2026-05-14): 各阶段进度推文
- yt-dlp Issue #16766: Bun EJS 兼容限制讨论