5月28日 03:10

Bun 的启动速度和依赖安装速度为什么快?

Bun 的启动速度和依赖安装速度之所以远超 Node.js 和 npm,核心原因在于它选择了完全不同的底层引擎和架构方案。下面从启动速度和依赖安装两个维度分别说明。

启动速度快的原因

Bun 启动速度远快于 Node.js,根本原因在于使用了 JavaScriptCore(JSC)引擎而非 V8,配合 Zig 语言编写的运行时实现。这和很多人印象中的"Bun 基于 V8"完全相反——Bun 从未使用 V8,它选择的是 WebKit/Safari 同源的 JSC 引擎。下面从引擎、语言、模块三个层面展开。

JavaScriptCore 的分层编译策略

JSC 采用四级编译流水线:LLInt → Baseline JIT → DFG JIT → FTL JIT。其中 LLInt(Low Level Interpreter)是关键——它可以直接解释执行字节码,跳过编译阶段。这意味着 Bun 在启动时不需要等待 JIT 编译完成,LLInt 直接开始执行代码,冷启动开销极低。

对比来看,V8 的编译管线是 Ignition(字节码解释器)→ Sparkplug(非优化编译)→ TurboFan(优化编译)。V8 的设计哲学是用启动时间换取峰值性能:先花时间编译出优化代码,再在长期运行中获得高吞吐量。这对长期运行的服务端应用很合适,但对于启动-执行-退出的短生命周期场景(如 CLI 工具、Serverless 函数、构建脚本),JSC 的渐进式编译策略显然更有优势。

具体到执行流程的差异:V8 启动时需要先解析 JavaScript 源码生成 AST,再将 AST 编译为字节码,接着由 Sparkplug 编译为非优化的机器码才开始执行。JSC 的 LLInt 可以直接解释执行字节码,无需等待机器码生成,第一个函数的执行延迟更低。之后随着代码反复执行,JSC 会逐步将热点代码提升到更高级别的 JIT 编译——从 Baseline JIT 到 DFG JIT,再到 FTL JIT,这和 V8 的优化方向一致,但起点更轻量。

根据 Bun 官方基准测试数据,Bun 的冷启动时间约为 8-15ms,而 Node.js 需要 60-120ms,Deno 需要 40-60ms。在 Serverless 场景下,这种 4-10 倍的启动速度差距会直接转化为用户可感知的延迟差异。2026 年已有团队将微服务从 Node.js 迁移到 Bun 后,获得了 60% 的延迟降低和 20% 的基础设施成本节省。

bash
# 本地冷启动对比 time bun run index.ts # real 0m0.008s time node index.js # real 0m0.065s time deno run index.ts # real 0m0.042s

为什么 JSC 选择了渐进式编译而 V8 选择了激进式编译?这和两个引擎的历史定位有关:JSC 最初是为 Safari 浏览器设计的,网页中 JavaScript 执行频繁但每次执行时间短,快速启动比峰值性能更重要;V8 是为 Chrome 和 Node.js 设计的,Node.js 服务端进程通常运行数天甚至数周,峰值吞吐量才是核心指标。Bun 选择 JSC 正是因为它更适合 CLI 和脚本场景——大部分脚本执行几秒就结束了,根本等不到 TurboFan 的优化生效。Deno 虽然也使用 V8,但通过 V8 Snapshot 技术将序列化的堆快照直接加载到内存,跳过了部分初始化步骤来加速启动,不过仍不及 JSC 的原生优势。

Zig 带来的原生性能优势

Bun 使用 Zig 语言编写(而不是 Rust),编译后生成单个静态链接的原生二进制文件。Zig 有几个对启动速度至关重要的特性:

  • comptime(编译期代码执行):Zig 允许在编译阶段执行代码,Bun 利用这个能力将大量初始化逻辑前移到编译期,运行时零开销。比如 Bun 内置的 Node.js 兼容 API 绑定就是在编译期生成的,不需要运行时动态注册和查找。这意味着当你 import { readFileSync } from 'fs' 时,对应的 Zig 实现已经在编译时链接好了,运行时只需要一次函数指针跳转。
  • 不依赖 libc:Zig 可以编译为不依赖系统 libc 的二进制文件,避免了动态链接库加载的开销,同时让交叉编译变得简单可靠。一个 bun 可执行文件就能在 macOS、Linux、Windows 上运行,无需额外依赖。对比 Node.js 的分发需要针对不同平台提供不同的二进制文件,且依赖系统级的动态库。
  • 手动内存管理,零隐藏分配:相比带 GC 的语言,Zig 没有运行时垃圾回收器的初始化和暂停开销,启动路径更短更可预测。这对 CLI 工具和脚本执行场景尤为重要——没有人希望自己的构建脚本因为 GC 停顿而变慢。

一个常见的误解是"Bun 用 Rust 写的所以快"。实际上 Bun 选择 Zig 而非 Rust,正是因为 Zig 的 comptime 能力和对底层控制的精确性更适合构建高性能运行时。Rust 的安全保证虽然有价值,但在运行时核心路径上,Zig 的显式控制更高效。Bun 的作者 Jarred Sumner 在多个技术演讲中解释过这个选择:Zig 的 comptime 让 Bun 可以在编译期生成大量胶水代码,而 Rust 的宏系统虽然强大但不如 comptime 灵活。另外 Zig 的交叉编译体验远优于 Rust——zig build 直接生成目标平台二进制,不需要配置复杂的 toolchain,这对 Bun 需要支持 macOS、Linux、Windows 三大平台的场景至关重要。

模块解析与加载优化

Bun 在模块系统上做了三层优化:

  1. 内置 Node.js 兼容 polyfillfspathhttpbuffercryptonetos 等常用模块直接内置于 Bun 运行时中,启动时无需从 node_modules 查找和加载,省去了文件系统 I/O。这些 polyfill 用 Zig 从零实现,比 JavaScript 实现的 polyfill 快得多。Node.js 的这些内置模块虽然是 C++ 编写,但需要通过 V8 的绑定层(N-API / NAN)桥接到 JavaScript,有额外的类型转换和异常处理开销;Bun 的 Zig 实现直接与 JSC 交互,调用路径更短。
  2. Transpiler 集成:Bun 内置了用 Zig 编写的 JavaScript/TypeScript transpiler,TypeScript 和 JSX 的转换在进程内完成,不需要启动外部进程(如 ts-nodeesbuild),避免了进程间通信的开销。这也是为什么 bun run 可以直接执行 .ts 文件而无需任何配置。内置 transpiler 的转换速度接近 esbuild,但省去了进程启动的额外开销。
  3. 懒加载策略:只在代码真正 import 时才解析和编译对应模块,而不是启动时全量加载整个模块图。对于包含数百个依赖的大型项目,这个优化能显著减少启动时的工作量——很多依赖可能根本不会被实际调用。
typescript
// Bun 直接运行 TypeScript,无需额外配置 bun run server.ts // 对比 Node.js 需要安装和配置 ts-node npx ts-node server.ts // 启动 ts-node 进程 → 加载 tsconfig → 编译 → 执行 // 或者使用 --loader 标志(更慢,且实验性) node --loader ts-node/esm server.ts

Bun 的 transpiler 虽然不如 tsc 严格(不做类型检查),但对于运行来说足够了。类型检查可以交给 IDE 和 CI 中的 tsc --noEmit 完成,运行时只需要语法转换。这种"编译时类型检查 + 运行时快速转换"的分工是 Bun 的设计理念之一,也是它比 ts-node 快一个数量级的原因——ts-node 每次运行都要完整编译整个项目,而 Bun 只转换当前需要执行的文件。

依赖安装速度快的原因

bun installnpm install 快约 25 倍,这来自包管理器架构层面的多个关键设计决策,而不仅仅是"用了更快的语言"。下面逐一拆解每个优化点。

全局缓存与硬链接机制

Bun 将所有下载过的包存储在全局缓存目录(~/.bun/install/cache/)中。安装依赖时,Bun 不会像 npm 那样把包复制到每个项目的 node_modules,而是:

  • Linux 上创建硬链接(hard link):多个项目的 node_modules 指向同一份磁盘数据,零拷贝开销。硬链接意味着不同项目引用的是同一份 inode,不占用额外磁盘空间。
  • macOS 上使用写时复制克隆(copy-on-write clone):利用 APFS 文件系统的 CoW 特性,同样实现零拷贝,且修改时不影响其他项目。

这意味着安装一个已缓存过的包几乎是瞬时完成的——只需要创建一个文件系统链接,不涉及网络请求、解压和文件写入。对比 npm 的流程:下载 tarball → 解压到临时目录 → 复制到 node_modules → 校验 integrity hash,Bun 直接跳过了前三步。

bash
# 查看全局缓存 ls ~/.bun/install/cache/ # 首次安装后,后续项目的安装几乎零开销 # 硬链接验证:两个项目的 node_modules 指向同一份数据 stat -c '%i' project-a/node_modules/lodash/index.js stat -c '%i' project-b/node_modules/lodash/index.js # 两个 inode 号相同,证明是硬链接

这种缓存策略带来的好处不仅是速度。想象你有 10 个项目都依赖 lodash@4.17.21:npm 会在每个项目中复制一份,占用 10 份磁盘空间;Bun 只存储一份,10 个项目通过硬链接共享,磁盘占用几乎不增加。在 CI/CD 环境中,缓存命中率更高,构建时间也更稳定可预测。这在大型 monorepo 中效果尤其明显——几十个子项目共享同一份全局缓存,安装时间几乎不随项目数量增长。

智能增量安装

当项目中已经存在 bun.lockpackage.json 未变更时,Bun 采用懒加载策略:只安装缺失的依赖。如果某个包已经存在于 node_modules 的预期位置,Bun 不会重新下载或校验,直接跳过。这使得二次安装几乎是瞬时完成。

这个策略和 npm 的行为有本质区别:npm 每次运行 npm install 都会重新校验 node_modules 中所有包的完整性(读取每个包的 package.json、比对版本号、验证 integrity hash),即使没有任何变更也要走一遍检查流程。在包含数百个依赖的大型项目中,这个校验过程可能耗时数秒到数十秒。Bun 的智能跳过策略将这个时间压缩到几乎为零。

并行执行生命周期脚本

npm 和 yarn 串行执行 postinstallpreinstall 等生命周期脚本,而 Bun 默认并行运行这些脚本,可以通过 --concurrent-scripts 参数调整最大并发数:

bash
# 调整并行脚本数 bun install --concurrent-scripts=8 # 完全禁用并行(排查问题时有用) bun install --concurrent-scripts=1

在包含大量带有 native 构建步骤的依赖时(如 node-sassbcryptsharpesbuildcanvas),并行执行生命周期脚本的提速效果非常显著。假设一个项目有 5 个需要编译的 native 模块,每个编译耗时 3 秒:npm 串行需要 15 秒,Bun 并行只需要约 3 秒。这也是为什么 Bun 在安装 native 依赖时优势特别明显。

高效的锁文件格式

Bun 使用二进制格式的 bun.lock 文件,相比 npm 的 JSON 格式 package-lock.json,解析速度更快、文件体积更小。一个典型的 package-lock.json 可能有几百 KB 甚至几 MB,而等价信息的 bun.lock 只有几十 KB。同时 Bun 的依赖解析算法在内存中操作依赖图,避免了 npm 反复读写 JSON 文件的 I/O 开销。

bash
# 对比锁文件大小 du -h package-lock.json bun.lock # 1.2M package-lock.json # 45K bun.lock

锁文件的解析速度在实际使用中影响很大:每次 npm install 开始时都要先解析整个 package-lock.json,构建出完整的依赖树,然后才能开始下载。当锁文件有数 MB 时,仅 JSON 解析就可能需要几百毫秒。Bun 的二进制格式解析几乎不需要时间,整个依赖树在微秒级就能重建完成。

网络层优化

Bun 的 HTTP 客户端同样用 Zig 实现,支持 HTTP/2 多路复用和连接复用,下载包时可以在同一个 TCP 连接上并行传输多个文件,减少握手开销。npm 使用的 Node.js HTTP 客户端在大量并发请求时容易遇到连接限制和超时问题,尤其是安装包含数百个依赖的大型项目时。Bun 使用的 Zig HTTP 客户端则没有这些限制,可以更充分地利用网络带宽。

此外,Bun 支持自动安装功能:当你运行 bun run 时,如果代码中引用了尚未安装的依赖,Bun 会自动下载并安装,不需要手动执行 bun install。这进一步简化了开发流程——写完代码直接运行,依赖会自动处理。对比传统工作流:先 npm installnode index.js,Bun 合并为一步 bun run index.ts,省去了上下文切换的额外时间。

值得注意的是,Bun 的速度优势并不是单一的"银弹",而是多个层面的优化叠加效果。JSC 引擎的编译策略、Zig 语言的编译期能力、全局缓存的硬链接策略、并行生命周期脚本——每一项单独拿出来可能只快几倍,但组合在一起就产生了 25 倍甚至更大的性能差距。理解这些底层原理,不仅能帮助你在面试中回答好这个问题,更能指导你在实际项目中做出正确的技术选型:如果你的应用是短生命周期的脚本或 Serverless 函数,Bun 的启动优势非常明显;如果是长期运行的服务端应用,Node.js 的峰值性能和生态成熟度可能更值得依赖。

追问:Bun 的速度优势有代价吗?

有。JSC 的渐进式编译意味着长时间运行后峰值性能可能不如 V8 的 TurboFan 优化——V8 会在运行过程中持续分析和优化热点代码,最终生成的优化代码执行效率更高。不过对于大多数 Web 开发场景,启动速度的收益远大于峰值性能的微小差异。另外 Bun 生态成熟度仍不如 Node.js,部分 npm 包可能存在兼容问题,生产环境使用前需要充分验证。

标签:Bun