你机器上的每个项目都依赖那几项沉重的公共包 ——Documentation Index
Fetch the complete documentation index at: https://bun.zhcndoc.com/llms.txt
Use this file to discover all available pages before exploring further.
typescript、next、webpack、@babel/*、react-dom。如果没有共享存储,每个 node_modules 都会各自保存一份副本,而每次新的 bun install 又会把它们全部重新写一遍。
全局虚拟存储将其变为安装一次,到处链接。包文件保存在一个共享缓存中;每个项目的 node_modules 都是一棵指向它的符号链接浅层树。第二次检出、新分支工作树、CI 工作区——它们都指向那份已经在磁盘上的副本。
结果是:热安装速度大约快 7 倍(每个包只需一个符号链接,而不是复制每个文件),而且 node_modules 会从每个项目的数百 MB 缩减到几 MB 的链接。
启用
全局虚拟存储默认关闭。它只适用于 isolated linker;hoisted linker 不会使用它。 要为某个项目启用它:bunfig.toml
terminal
globalStore = false 或 BUN_INSTALL_GLOBAL_STORE=0。
Why it’s fast
之前的 isolated linker 会在每次安装时,对每个包都调用clonefileat()(macOS)或 link()/copyfile()(其他平台),即使包缓存已经是热的,而缺失的只有 node_modules。对一个 1,400 包的 fixture 在 macOS 上进行热安装的分析显示,主线程有 95.4 % 的时间都花在 clonefileat 里:
clonefileat 会持有一个全卷范围的内核锁,因此即使增加更多线程,收益也很小——8 个线程只把一个 2,830 目录的克隆从 959 ms 提升到 743 ms。修复方法就是在热路径上根本不调用它。
有了全局存储,热路径对每个包只需要一次 access()(全局条目是否存在?)再加一次 symlink()(把项目指向它)。
基准测试
热态 CI 安装——锁文件存在、包缓存已热、每轮之间删除node_modules——在一个 1,400 包的 React/webpack/Babel/jest fixture 上,Apple Silicon macOS,hyperfine --warmup 3 --runs 10:
| wall time | system time | clonefileat | total syscalls | |
|---|---|---|---|---|
--linker hoisted | 823.9 ms | 477 ms | 1,387 | 7,857 |
--linker isolated, globalStore=false | 840.9 ms | 1,256 ms | 1,387 | — |
--linker isolated, global store | 124.8 ms | 94 ms | 0 | 4,957 |
磁盘
同一 fixture 的node_modules 大小(APFS 上运行 du -sh node_modules;clonefile 复制是写时复制,所以 hoisted/每项目的数字是 逻辑 大小——在不支持 CoW 的文件系统上,这也就是物理大小):
每个项目的 node_modules | 磁盘上的共享占用 | |
|---|---|---|
--linker hoisted | 391 MB | — |
--linker isolated, globalStore=false | 391 MB | — |
--linker isolated, global store | 约 5 MB 的符号链接 | 391 MB 仅一份 |
真实世界 cold→warm
克隆后的真实仓库在冷到热之间的耗时(macOS arm64):| project | packages | cold | warm |
|---|---|---|---|
| cal.com | ~3,580 | 37.4 s | 4.7 s |
| remix | ~1,750 | 23.1 s | 2.0 s |
| excalidraw | ~1,332 | 5.9 s | 1.1 s |
| hono | ~790 | 4.5 s | 1.3 s |
next build (create-next-app) | ~382 | 1.0 s | 0.35 s |
目录结构
与 isolated installs 相比,磁盘上的布局多加了一层间接层:tree layout
entry_hash 后缀编码的是该条目的已解析依赖闭包:包自身的 store path 和 tarball 完整性,再加上它所链接到的每个依赖的哈希。两个项目如果把 react@18.3.1 解析到同一组传递版本,就共享同一个全局目录;如果某个项目把某个传递依赖解析到不同版本,就会得到一个单独的全局条目,其依赖符号链接会指向正确的同级项。参与依赖循环的包会共享一个基于整个强连通分量计算出的哈希,因此这个 key 不依赖于某个项目的依赖图最先碰到的是哪个成员。
哪些内容保持项目本地
只有在条目能够安全共享时,它才会存在于全局存储中。以下情况会回退到每个项目各自的node_modules/.bun/<storepath>/ 目录:
- 包通过
bun patch应用了 patch —— 补丁后的内容是项目专属的; - 包列在
trustedDependencies中(或通过bun add --trust被信任)—— 它的生命周期脚本可能会修改安装目录,而通过项目符号链接运行脚本会修改共享副本; - 该包,或它链接到的任何依赖,是
workspace:、file:或link:依赖 —— 这些会解析到项目本地路径,其他项目看不到。
your-app 依赖于一个工作区包 internal-utils,那么 internal-utils 是项目本地的,所有链接到它的条目也都是项目本地的。在安装之间失去资格(新打补丁、新被信任)的条目会从全局存储中拆离,并在下一次安装时以项目本地方式重建;共享条目保持不变。
peer 依赖
已解析的 peer 依赖——必需和可选的——会作为依赖符号链接并入每个全局条目,并计入其哈希。Bun 会为那些只在peerDependenciesMeta 中列出名称、却没有对应 peerDependencies 条目的包合成一个隐式的 "*" 可选 peer(与 pnpm 和 yarn 一致),因此像 webpack 这样只在 peerDependenciesMeta 中声明 webpack-cli 的包,在项目中安装了 webpack-cli 时,仍然会在其全局条目里得到一个 webpack-cli 符号链接。
取舍
幽灵依赖回退
当包位于项目的node_modules/.bun/ 下时,Node 的模块解析会沿着 node_modules/.bun/node_modules/ —— 这个隐藏的 hoisted 层 —— 向上查找,然后才到项目根目录。有了全局存储后,包会 realpath 到 <cache>/links/,因此从包内部看,这一层就不再位于解析路径上。
实际中这只会影响真正的幽灵依赖:某个包 require('helper'),但它从未在 dependencies、peerDependencies 或 peerDependenciesMeta 中声明过这个东西。如果遇到这种情况,把 helper 加到消费方包的依赖里(这是正确修复方式),或者设置 globalStore = false。
注意,publicHoistPattern 和 hoistPattern 会提升到项目的 node_modules,而全局存储中的包无法访问那里。不过它们仍然可以用于从你自己的源代码中解析被提升的包。
node_modules 大多是符号链接
不会跟随符号链接就扫描 node_modules 的工具,或者通过字符串相等比较文件路径的工具,可能会表现不同。这与任何 pnpm 风格布局的注意事项相同。
磁盘使用
每个唯一的(package, version, resolved-dependency-set) 三元组都会在 <cache>/links/ 中得到一个目录。跨多个项目来看,这是巨大的净收益——磁盘上只存一份,而不是每个检出一份——但随着新版本和新的 peer-dependency 组合不断出现,存储也会逐渐增长。运行 bun pm cache rm 可清除包括全局存储在内的缓存;下一次安装只会重新填充该项目所需的内容。
并发
多个bun install 进程(并行 CI 作业、并发工作区构建)可能会竞争填充同一个全局条目。每个进程都会在私有的 <entry>.tmp-<random>/ 暂存目录下构建整个条目——包文件、依赖符号链接、bin 链接——并在最后一步将其重命名到位。重命名失败的一方会看到 EEXIST,并丢弃自己相同的暂存树;在构建过程中崩溃的写入者只会留下一个无人引用的暂存目录,下一次安装会忽略它。因此,已发布的条目总是完整的;不存在单独的完整性哨兵。
相关文档
- 包管理器 > 隔离安装 — 全局存储所依赖的链接器
- 包管理器 > 全局缓存 — 已下载包的存储位置
- 运行时 > bunfig —
bunfig.toml参考