CI 里 npm ci 比本地 npm install 还慢,偶尔还报 EINTEGRITY
团队把流水线从「每次 npm install」换成「npm ci」之后,构建时间不降反升:本地跑 `npm install` 一分半,流水线里 `npm ci` 要四分半,而且两周内失败了三次,报错都是 `EINTEGRITY`。花了一个下午查清楚,是三个独立的问题叠在一起。
一、npm ci 到底做了什么
先把「为什么 npm ci 更该在 CI 里用」说清楚,再说它为什么在这里变慢。
`npm ci` 的行为是固定的:
1. 如果 `package-lock.json` 和 `package.json` 不一致,直接报错退出(这其实是好事,能挡住「本地装完忘了提交 lock」);
2. **删掉整个 `node_modules`**,然后完全按 lockfile 里的 `resolved` 地址和版本重新装一遍;
3. 不做版本求解,所以理论上比 `npm install` 快得多。
它慢只有一种可能:**每次都在从零下载**。这也是第一个坑。
二、坑一:缓存的目录错了
很多流水线模板缓存的是 `node_modules` 目录。这个做法对 `npm ci` 完全无效——它第一步就把 `node_modules` 删了,缓存恢复出来的东西一行都用不上,反而多花几十秒解压。
真正该缓存的是 npm 的全局缓存目录:
```bash
npm config get cache # 一般是 ~/.npm
```
流水线里对应地缓存 `~/.npm`(Windows 上是 `%LocalAppData%\npm-cache`),缓存 key 用 lockfile 的哈希:
```yaml
cache:
key: npm-${{ hashFiles('package-lock.json') }}
paths:
- ~/.npm
```
配上之后,第二次构建就从本地缓存装,`npm ci` 会自己优先用缓存里的 tarball。实测从 4 分半降到 1 分 10 秒。
再加两个参数,能省掉一堆没必要的网络请求:
```bash
npm ci --prefer-offline --no-audit --no-fund
```
`--prefer-offline` 是「缓存里有就直接用,不再校验远端」;`--no-audit` 关掉安装后的漏洞扫描(它会额外发一次 bulk advisory 请求,国内网络下经常超时重试);`--no-fund` 只是少打印点东西。三项加起来又能省 20 秒左右。
三、坑二:lockfile 里的 resolved 指向了另一个源
这个问题更隐蔽。`package-lock.json` 里每个包都有 `resolved` 字段,写着 tarball 的完整下载地址:
```json
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-..."
}
```
如果流水线用 `.npmrc` 把 registry 改成了内网镜像,npm 会怎么处理?答案是**看版本**:
- npm 7 及以上:如果 `resolved` 的 host 和当前 registry 不一致,会**改写成当前 registry** 再下载;
- 更早的版本(以及某些 npm 兼容实现):直接按 `resolved` 下载,`.npmrc` 里的 registry 形同虚设。
我们踩的是第二种。流水线为了能访问,配了内网镜像,但 lockfile 是开发者在本机用官方源生成的,结果安装阶段仍然在往 `registry.npmjs.org` 发请求,走代理超时重试,这才是四分半的主要来源。
判断方法很简单,看安装日志里的下载地址:
```bash
npm ci --loglevel=http 2>&1 | grep -o 'https://[^ ]*tgz' | sort -u | head
```
如果打印出来的 host 不是你配的那个源,就是这个问题。
修法有两个,选一个即可:
1. 在流水线里统一执行一次 `npm install --package-lock-only`,让 lockfile 的 `resolved` 改写成内网地址,然后提交(缺点是开发者的本地环境也得用同一个源);
2. 更推荐:**保持 lockfile 用官方源地址**,只把 npm 的 registry 换掉,并确认流水线用的 npm 版本在 7 以上。这样 lockfile 是源无关的,谁都能用。
顺带说一句,`resolved` 里如果写的是 `http://` 而不是 `https://`,某些新版 npm 会直接拒绝,报的错和源不可达很像,别被误导。
四、坑三:EINTEGRITY 不是文件坏了,是镜像没同步完
三次失败都是这个:
```
npm error code EINTEGRITY
npm error sha512-xxxxx integrity checksum failed when using sha512
```
`integrity` 是 tarball 内容的 sha512,和从哪个源下载无关。所以这个错误的含义只有一个:**下载到的内容和 lockfile 里记录的不一致**。
常见原因按概率排:
1. **镜像源同步延迟或同步出错**。上游发了新版本,镜像抓了一半,或者抓的是重新打包过的 tarball(有些私有代理会重打包,字节变了哈希也变了)。表现是「同一个包,昨天能装今天不能装」。
2. **缓存里存了半个文件**。上一次构建被中断,tarball 只写了一部分但缓存认为有效。清缓存即可:`npm cache verify`,必要时 `npm cache clean --force`。
3. **私有包的版本被覆盖发布**。npm 允许 `--force` 重发同一版本,哈希就变了,而 lockfile 里还是旧哈希。这种情况只能重新生成 lockfile。
排查顺序:先 `npm cache verify`(它会校验缓存并清掉坏条目),不行就换个源重试,再不行才怀疑 lockfile。
五、最终效果和一条经验
改完之后流水线是:缓存 `~/.npm` + `npm ci --prefer-offline --no-audit --no-fund` + 固定 npm 版本(`corepack` 或 `packageManager` 字段)。安装阶段从 4 分 30 秒降到 45 秒,两个月没再出现 EINTEGRITY。
一条经验值得单独记下来:**`npm ci` 的价值是「可复现」,不是「快」**。它快是因为不用求解版本,但如果缓存没配对,它比 `npm install` 更慢——因为它一定会从零开始。所以换 `npm ci` 的同时必须把缓存目录改对,这两件事是一套的。
- 点赞
- 收藏
- 关注作者
评论(0)