# npm 缓存越用越大，Mole 这次只清不再被引用的部分

> 用缓存副本比较两种清理方式，说明 npm 为什么按引用清理，Go 为什么按使用时间清理，以及两处漏掉的构建产物。

Published: 2026-10-05

最近有用户提到，用 Mole 维护完 Mac，npm 的缓存还在，自己清理以后从 8 GB 变成了 2 GB。这条反馈让我重新看了一遍开发工具的缓存，哪些东西还能用，哪些已经没有引用，还有哪些工具自己就在清，Mole 再清一次反而让下一次构建多花时间。

这次调研和改动发生在 10 月 5 日，代码已经补上，会随后续版本交付，当前已发布的 Preview 包还没有带上，下面写的是这次开发过程中的实测和取舍。

## npm 的缓存为什么会越用越大

npm 默认把缓存放在 `~/.npm/_cacache`，里面有两部分，`content-v2` 保存软件包和软件源返回的内容，`index-v5` 记录从哪个请求能找到哪份内容，文件按内容的哈希命名，同一份内容可以被多个索引条目引用。

软件包的信息更新后，新内容会被写进去，旧索引条目在后续查询时可能被整理掉，旧文件却还留着，时间一长，就积累出一批已经没有索引指向的文件。[npm 的文档](https://docs.npmjs.com/cli/v11/commands/npm-cache/)也说明，缓存不会自行清理，会随着安装继续增长。

不过那位用户已经清过自己的缓存，我没拿到清理前的副本，也不能确认他具体跑了哪个命令，所以 8 GB 降到 2 GB 是这次调查的起点，不能当成 Mole 已经清出了 6 GB，更不能说这 6 GB 全是没有引用的内容。

## 副本实验里，verify 也删掉了仍然能用的内容

这次实验用的是本机缓存的副本，npm 11.19.1、cacache 20.0.4，原缓存没有改动，它刚建立约一天，里面的内容文件一共 309796162 字节，还不是一份积累了几个月的缓存。

| 判定方式 | 内容文件数 | 内容字节数 |
| --- | --- | --- |
| 没有任何有效索引条目引用 | 1 | 614295 |
| `npm cache verify` 实际删除 | 6 | 25469972 |

差别来自 [cacache 的 verify 实现](https://github.com/npm/cacache/blob/v20.0.4/lib/verify.js)，它清理内容、校验完整性，再重建索引，同一个 key 只保留最后一条，旧条目里却可能还有不同请求需要的完整元数据和精简元数据，这两种内容不能简单当成一份的旧版本。

在副本上，verify 删除的内容里有 11199796 字节仍被有效的另一种请求条目引用，清理前能离线读取的几个软件包信息，清理后再跑离线 `npm view` 就报了 `ENOTCACHED`，联网以后可以重新获取，但这已经改变了缓存原来能做的事。

所以这里选了更窄的判定，只要任何一条有效索引还引用着它，就留着，哪怕这次只能找到约 0.6 MB，也不为了接近 verify 的数字去删另一种请求仍然需要的内容。

## 放进清理页，保留整个缓存的选择

Mole 的新实现只扫描默认的 `~/.npm/_cacache`，没有引用、超过一天的内容会合成一行，放在清理页的开发工具分组里，默认勾选，整个 npm 缓存仍然需要自己确认，两种选择重叠的部分也只计算一次。

扫描时遇到 `npm install`、`npm ci` 这类正在写缓存的命令就先不处理，读不清索引也不列出来，删除前还会重新确认引用，索引文件和临时目录都不改，Mole 也不会在后台调用 npm 的清理命令。

这里保留的是索引仍然指向的内容，不等于保证每一个项目永远能离线构建，锁文件还可能按哈希直接找某个软件包，私有软件源和旧版本也未必始终能重新下载，源码、锁文件和需要保留的产物仍然要单独保存。如果配置了别的 npm 缓存路径，这一轮也没有覆盖。

## Go、Cargo 和 pnpm，不能照着 npm 再做一遍

顺着这条反馈，又看了其他工具。Go 本来就会自动整理构建缓存，[实现里](https://go.dev/src/cmd/go/internal/cache/cache.go)保留最近五天用过的条目，再留一小时的时间余量，也就是 121 小时，Mole 以前把整个构建缓存目录列进默认清理，这次改成同样的时间规则，最近还要用的条目留下来，不用在下一次构建时全部重新编译。

Cargo 已经有[自动缓存回收](https://doc.rust-lang.org/cargo/reference/config.html#cache)，这次样本里没有到期内容，之前清掉从软件包仓库解压出的源码，约 29 小时后又下载了约 157 MB、解压出约 1.06 GB，所以这次没有再做一份 Cargo 自动回收，原来需要自己确认的清理项仍然保留。

pnpm 的共享存储又不同，调研里的 APFS 样本不能靠硬链接数量证明文件已经没人用，照搬这个判据会把整份存储都选出来，所以这次没有加一份 pnpm 清理，把一个工具的判定搬到另一个工具上，先得确认它说的使用关系在本机文件系统里仍然成立。

## 全局工具和旧 Xcode 项目里的构建产物

调研时，全局安装的 `pake-cli` 里面有一个 `src-tauri/target`，约 1.48 GiB，是它构建 Tauri 时留下的 Rust 产物，并不是 npm 下载缓存。现在这种由工具写下有效 `CACHEDIR.TAG` 标记的构建目录，也会作为待确认项列出来，工具本身和它的 `node_modules` 依赖不会跟着删，Rust 构建还在运行时也先留着。

Xcode 那边还找到 24 份原项目路径已经不存在的 DerivedData，调研时约 9 GiB，这次只有启动磁盘上的项目能确认已经删掉、Xcode 至少两周没有使用过对应缓存，才会列出整个目录，外接盘没接上、没有读取权限，都不能算作项目不存在。这两个大小是当时找到的内容，不是承诺每台 Mac 都能释放这么多，清掉以后再需要构建，仍然要花时间重新生成。

## 自己处理时，先确认正在清哪一层

想看自己的 npm 缓存在哪，可以先运行下面这个只读命令，自定义路径会由 npm 自己报告。

```bash
npm config get cache
```

确认没有安装和构建正在写这份缓存，再决定是否运行 `npm cache verify`，它会实际修改缓存，不只是查看，项目的 `node_modules` 和全局安装的命令又是另外两层，不会被这个命令一起处理，完整的整理顺序可以接着看[开发工具缓存清理](https://mole.fit/zh/blog/how-to-clear-dev-caches-mac)。

---

Canonical HTML page: https://mole.fit/zh/blog/mole-developer-cache-gc
Blog index for agents: https://mole.fit/zh/blog/llms.txt
Site index for agents: https://mole.fit/llms.txt
