typescript、next、webpack、@babel/*、react-dom。没有共享存储,每个 node_modules 都有自己的副本,每次全新的 bun install 都要重新写入它们。
全局虚拟存储改变了这一点,实现一次安装,处处链接。包文件存在于一个共享缓存中;每个项目的 node_modules 是指向该缓存的一个薄层符号链接树。第二次检出、新的分支工作树、CI 工作空间——它们都指向磁盘上已有的副本。
结果是:热安装大约快 7 倍(每个包一个符号链接,而不是复制每个文件),而 node_modules 从每个项目数百兆字节缩减到几兆字节的链接。
启用
全局虚拟存储默认关闭。它仅适用于隔离链接器;提升链接器不使用它。 要为项目启用它:bunfig.toml
terminal
globalStore = false 或 BUN_INSTALL_GLOBAL_STORE=0。
为什么它很快
之前的隔离链接器在每次安装时为每个包调用clonefileat()(macOS)或 link()/copyfile()(其他平台),即使包缓存是热的,唯一缺少的是 node_modules。对 macOS 上一个包含 1,400 个包的测试用例进行热安装分析显示,主线程在 clonefileat 中花费了 95.4% 的时间:
clonefileat 持有一个卷级的内核锁,因此将工作分散到更多线程上几乎没有帮助——八个线程仅将 2,830 个目录的克隆从 959 毫秒改善到 743 毫秒。解决方法是在热路径上完全不调用它。
使用全局存储,热路径是每个包一次 access()(检查全局条目是否存在)加上一次 symlink()(将项目指向它)。
基准测试
热 CI 安装——锁文件存在,包缓存热,node_modules 在运行之间删除——在一个包含 1,400 个包的 React/webpack/Babel/jest 测试用例上,Apple Silicon macOS,hyperfine --warmup 3 --runs 10:
比没有全局存储的同一链接器快 6.6 倍,比提升链接器快 6.6 倍。
磁盘
相同测试用例的node_modules 大小(du -sh node_modules on APFS;clonefile 副本是写时复制的,因此提升/每项目数字是_逻辑大小_——在不支持 CoW 的文件系统上,这也是物理大小):
将同一个项目克隆五次,差异大约是约 2 GB 的重复包文件对约 400 MB 总量。一个项目就达到收支平衡;之后每次额外的检出、分支工作树或 CI 工作空间都是免费的。
真实世界冷→热
在克隆的真实世界仓库上的冷到热计时(macOS arm64):目录结构
磁盘布局相比于隔离安装增加了一层间接性:tree layout
entry_hash 后缀编码了条目的已解析依赖闭包:包自身的存储路径和 tarball 完整性,加上它链接到的每个依赖的哈希。将 react@18.3.1 解析为相同传递版本集的两个项目共享一个全局目录;将传递依赖解析为不同版本的项目会获得一个单独的全局条目,其依赖符号链接指向正确的兄弟版本。参与依赖循环的包共享一个在整个强连通分量上计算的哈希,因此该键独立于给定项目的依赖图首先到达哪个成员。
什么保留在项目本地
只有当条目可以安全共享时,它才会存在于全局存储中。当以下情况时,条目会回退到每个项目的node_modules/.bun/<storepath>/ 目录:
- 包通过
bun patch应用了补丁——修补后的内容是项目特定的; - 包被列在
trustedDependencies中(或通过bun add --trust信任)——其生命周期脚本可能会修改安装目录,而通过项目符号链接运行的脚本会修改共享副本; - 该包或它链接到的任何依赖是
workspace:、file:或link:依赖——这些解析为其他项目无法看到的项目本地路径。
your-app 依赖于 internal-utils,而后者是一个工作空间包,那么 internal-utils 是项目本地的,链接到它的每个条目也是如此。在安装之间失去资格的条目(新修补的、新信任的)会与全局存储分离,并在下次安装时在项目本地重建;共享条目保持不变。
对等依赖
已解析的对等依赖(必需的和可选的)作为依赖符号链接折叠到每个全局条目中,并参与其哈希计算。Bun 会为那些仅在没有匹配peerDependencies 条目的 peerDependenciesMeta 中列出名称的包合成一个隐式的 "*" 可选对等依赖(与 pnpm 和 yarn 一致),因此像 webpack 这样仅在 peerDependenciesMeta 中声明 webpack-cli 的包,当项目中安装了 webpack-cli 时,仍会获得一个 webpack-cli 符号链接。
权衡
幽灵依赖回退
当包位于项目的node_modules/.bun/ 下时,Node 的模块解析会在到达项目根之前向上遍历 node_modules/.bun/node_modules/——即隐藏的提升层。使用全局存储时,包的真实路径位于 <cache>/links/,因此该层不再位于从包内部开始的解析路径上。
在实践中,这仅影响真正的幽灵依赖:某个包对从未在 dependencies、peerDependencies 或 peerDependenciesMeta 中声明的东西执行 require('helper')。如果遇到此问题,请将辅助包添加到消费包的依赖中(正确的修复方法)或设置 globalStore = false。
publicHoistPattern 和 hoistPattern 会提升到项目的 node_modules 中,全局存储内的包无法访问这些目录。它们对于从你自己的源代码解析提升的包仍然有效。
node_modules 主要是符号链接
不跟随符号链接扫描 node_modules 的工具,或通过字符串相等比较文件路径的工具,可能表现不同。这与任何 pnpm 风格布局的注意事项相同。
磁盘使用
每个唯一的(package, version, resolved-dependency-set) 三元组在 <cache>/links/ 中获得一个目录。跨多个项目,这是一个巨大的净收益——磁盘上一份副本而不是每个检出一份——但随着新版本和新对等依赖组合的出现,存储确实会随时间增长。运行 bun pm cache rm 清除包括全局存储在内的缓存;下次安装仅会重新填充该项目所需的内容。
并发
多个bun install 进程(并行 CI 作业、并发工作空间构建)可能竞相填充同一个全局条目。每个进程在一个私有的 <entry>.tmp-<random>/ 暂存目录下构建整个条目——包文件、依赖符号链接、bin 链接——然后将其重命名为最终位置。重命名失败的进程看到 EEXIST 并丢弃其相同的暂存树;在构建过程中崩溃的写入者只留下一个未引用的暂存目录,下次安装会忽略它。因此,已发布的条目始终是完整的;没有单独的完整性标记。
相关文档
- 包管理器 > 隔离安装 — 全局存储所基于的链接器
- 包管理器 > 全局缓存 — 下载的包存储的位置
- 运行时 > bunfig —
bunfig.toml参考