> ## 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 不会使用它。

要为某个项目启用它：

```toml title="bunfig.toml" icon="settings" theme={"theme":{"light":"github-light","dark":"dracula"}}
[install]
linker = "isolated"
globalStore = true
```

或者通过环境变量按次启用：

```bash terminal icon="terminal" theme={"theme":{"light":"github-light","dark":"dracula"}}
BUN_INSTALL_GLOBAL_STORE=1 bun install --linker isolated
```

要显式关闭它（默认行为），请设置 `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` 里**：

```
按栈顶排序（bun install --linker isolated，热缓存）：
    clonefileat        891 / 934 samples
    __openat            15
    __read_nocancel     10
```

APFS 上的 `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**      |

比没有全局存储的同一 linker **快 6.6 倍**，比 hoisted **快 6.6 倍**。

### 磁盘

同一 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 仅一份 |

把同一个项目克隆五份，差别就是大约 2 GB 的重复包文件 versus 总计约 400 MB。盈亏平衡点是一个项目；从那之后，每增加一个检出、分支工作树或 CI 工作区都是免费的。

### 真实世界 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](/pm/isolated-installs#directory-structure) 相比，磁盘上的布局多加了一层间接层：

```bash tree layout icon="list-tree" theme={"theme":{"light":"github-light","dark":"dracula"}}
~/.bun/install/cache/
├── react@18.3.1@@@1/                     # 包缓存（不变）
│   └── ...package files...
└── links/                                # 全局虚拟存储
    └── react@18.3.1-5664d3cd670b3205/    # <storepath>-<entry hash>
        └── node_modules/
            ├── react/                    # 包文件（只创建一次）
            ├── loose-envify -> ../../loose-envify@1.4.0-ea24…/node_modules/loose-envify
            └── .bin/
                └── ...

project/node_modules/
├── .bun/
│   ├── node_modules/                     # 隐藏的 hoisted 层（每个项目）
│   │   └── react -> ../react@18.3.1/node_modules/react
│   └── react@18.3.1 -> ~/.bun/install/cache/links/react@18.3.1-5664d3cd670b3205
└── react -> .bun/react@18.3.1/node_modules/react
```

16 位十六进制的 `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`](/runtime/bunfig#install-publichoistpattern) 和 [`hoistPattern`](/runtime/bunfig#install-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`，并丢弃自己相同的暂存树；在构建过程中崩溃的写入者只会留下一个无人引用的暂存目录，下一次安装会忽略它。因此，已发布的条目总是完整的；不存在单独的完整性哨兵。

## 相关文档

* [包管理器 > 隔离安装](/pm/isolated-installs) — 全局存储所依赖的链接器
* [包管理器 > 全局缓存](/pm/global-cache) — 已下载包的存储位置
* [运行时 > bunfig](/runtime/bunfig#install-globalstore) — `bunfig.toml` 参考
