
























做前端做久了,多少都会被 node_modules 气到一次。
明明业务代码没多少,仓库一拉下来,磁盘先掉几百 MB。再来几个子项目,风扇开始转,空间开始红,最后你只能盯着 Finder 里那个熟悉的文件夹发呆。
我以前对这事的态度也很典型:知道它大,但懒得追。直到本地项目越来越多,机器空间越来越紧,我才认真拆了一次,想看看到底是谁在吃磁盘。
拆完之后,结论其实不复杂:
很多时候不是“代码大”,而是“开发环境大”。
真正占地方的,往往是 TypeScript、构建工具、本地运行时、图片处理库、测试环境这些东西。业务源码反而没你想的那么重。
也是因为这次排查,我后面基本把新项目都切到了 pnpm。它不是什么银弹,但在“本地有很多 Node 项目”这个场景里,确实比 npm 省心。
最表层的原因当然是依赖多,但这句话没什么帮助。更准确一点说,node_modules 容易膨胀,通常是几个因素叠在一起。
先是依赖树本来就深。你在 package.json 里看到的是几十个直接依赖,落到磁盘上往往已经变成一大片间接依赖。
装一个构建工具,背后一般会跟出来这些东西:
表面上你只装了一个包,实际上你是把一整串开发链一起搬回来了。
第二个问题更实际:不同项目会重复存同样的依赖。
如果你电脑里只有一个 Node 项目,这个问题还不算明显。可一旦项目变成 5 个、10 个,重复成本就会很夸张。
比如 react、typescript、vite、eslint 这种常见依赖,很多项目都会用。传统安装方式下,这些东西很容易在每个项目的 node_modules 里各放一份。
单看一个仓库好像没什么,放到整台机器上看,就很肉疼。
pnpm 官方文档讲得很直接:如果有 100 个项目都用同一个依赖,npm 可能会在磁盘上留下 100 份;pnpm 则会把包存到一个统一的内容寻址存储里,再通过链接复用。
来源:pnpm Motivation
第三个问题是版本不一致。
很多重复不是因为“你装太多了”,而是因为同一个包在不同地方解析成了不同版本。哪怕只差一点点,也没法完全复用。
比如:
foo@^1.0.0foo@^1.1.0foo@~1.1.2看起来都是 foo,最后落下来的未必是同一个版本。于是磁盘上就得保留多份。
最后还有个经常被忽略的点:node_modules 不只是大,它还很碎。
目录多、小文件多,会让下面这些事情都变得更慢:
所以你感受到的往往不只是“占空间”,而是整个开发环境都显得很笨。
前面这些还是泛泛而谈。真正让我下决心换包管理器的,是我把手头项目拆开看了一次。
当时有三块体积特别显眼:
node_modules:262Mworker:169Mwebsite:147M第一眼确实有点吓人。但继续拆下去以后,事情反而变简单了:业务代码没那么重,真正重的是每个子项目背后的工具链。
node_modules 会有 262M这 262M 基本就是主扩展项目自己的前端构建环境。
占得比较多的有:
typescript:22Mhappy-dom:16Mesbuild:单个大约 9Mvite / rollup / jsdom这里还有个很容易看走眼的点。因为项目本身已经在用 pnpm,所以 node_modules 顶层很多包看起来像 0B。那不是真的没占空间,只是软链接。真正的内容在 node_modules/.pnpm/ 里。
所以这 262M 不是“代码写多了”,而是 TypeScript、构建器、DOM 模拟环境这些开发依赖在占地方。
worker 会有 169Mworker 也差不多,大头几乎全在 worker/node_modules/.pnpm/。本质上是 Cloudflare Worker 这一套本地开发链很重。
主要是这些:
@cloudflare/workerd-darwin-arm64:83Mtypescript:23M@img/sharp-libvips-darwin-arm64:15M@cloudflare/workers-types:10Mwrangler:6.8Mworkerd:4.2Mminiflare:3.0M换句话说,重的不是业务逻辑,而是:
website 会有 147Mwebsite 也是同样的路数。主要体积都在 website/node_modules/.pnpm/,背后是 Astro 那套静态站点构建链。
比较显眼的几项:
typescript:23M@img/sharp-libvips-darwin-arm64:15Mesbuild:各 9-10M@shikijs/langs:9.8Mastro:5.4M@astrojs/compiler:5.1M再加上 zod、shiki、prismjs 这些,单个看着还行,堆起来就不小了。
所以这 147M 主要还是构建、Markdown 解析、代码高亮相关的依赖,不是网站源码本身。对应的 website/src,当时其实只有 168K。
这次排查最有意思的地方就在这儿:
node_modules 262M:主扩展构建链worker 169M:Cloudflare Worker 本地开发链website 147M:Astro 官网构建链真正小的是源码,真正重的是开发环境。
所以很多时候你去折腾业务代码体积,对本地磁盘没什么帮助。空间压力常常不在 src/,而在 TypeScript、构建器、运行时、测试环境、代码高亮、图片处理这些配套工具上。
碰到 node_modules 太大,最常见的反应一般有三种。
第一种,删了重装。
最常见的命令大概是这样:
rm -rf node_modules package-lock.json
npm install
这招有时能清掉一点历史残留,但本质上只是重建,不是瘦身。如果依赖结构没变,装回来还是一样大。
第二种,跑 npm dedupe。
npm dedupe 会尝试把依赖往上提,减少重复项。npm 官方文档的描述是:它会搜索本地依赖树,并尝试简化整体结构,让多个依赖更有效地共享同一个包。
来源:npm dedupe 文档
这当然不是没用,但边界也很明显:
所以它更像收拾房间,不是搬家。
第三种,告诉自己以后少装点包。
方向没错,但实际很难靠意志力解决。
因为现在很多体积不是你主动选出来的,而是构建工具、测试工具、代码规范工具一路带出来的。你当然可以克制一点,但光靠“少装几个包”很难真正见效。
pnpm 为什么能明显省空间我后来切到 pnpm,最有价值的地方其实不是“更快”,而是它换了一种存储方式。
简单说,pnpm 不会让每个项目都老老实实拷一整份依赖。
它用的是 content-addressable store,也就是内容寻址存储。可以粗暴理解成这样:
node_modulespnpm 官方文档也专门提到这一点:同版本依赖可以跨项目共享,哪怕版本更新,也只需要为真正变化的文件增加存储。
来源:pnpm Motivation
它还有一个容易让人误会的地方:目录看起来有点怪。
很多人第一次看 pnpm 的 node_modules 都会有点愣,因为里面会多出一个 .pnpm 目录,再通过一层层链接把依赖串起来。
官方文档的解释其实挺清楚:
node_modules 中真正的文件会硬链接到全局 storepnpm Symlinked node_modules structure所以它不是把依赖变没了,而是少做了很多没必要的重复拷贝。
还有一点我自己挺喜欢:它更严格。
传统扁平化 node_modules 有个老问题:有些包明明没写进 package.json,但因为被提升到根目录,项目里居然也能跑。
这种隐式依赖平时不一定出事,一换环境就容易炸。pnpm 默认更严格,通常会更早把这类问题暴露出来。官方文档也提到,非扁平结构的一个好处就是,只有真实依赖图里的包才可访问。
来源:pnpm Symlinked node_modules structure
pnpm 的我切 pnpm 的目标挺朴素,不是为了跑分,就是想解决两个现实问题:
node_modules 的成本越来越高最后我的做法也很简单:新项目直接用 pnpm,老项目慢慢迁。
安装方式不复杂。
如果 Node 版本比较新,我更建议直接用 Corepack:
corepack enable
corepack prepare pnpm@latest --activate
当然也可以全局装:
已有项目切过去,一般这样就够了:
rm -rf node_modules package-lock.json
pnpm install
如果仓库之前用的是 Yarn,那就把 yarn.lock 也清掉。一个仓库最好只留一种包管理器。
我后来顺手把团队里的习惯也统一了:
pnpm-lock.yamlpnpm installpnpm install --frozen-lockfile真正的体感变化,其实不在某一个项目上。
切过去之后,最明显的不是某个项目突然小了 90%,而是整台机器上的 Node 项目没那么臃肿了。感受大概来自这几件事:
尤其是你手上同时有这些东西的时候:
这时候 pnpm 的优势会比单仓库明显很多。
另外,如果你只是想临时回收一点空间,最有效的办法通常也不是删源码,而是删暂时不用的构建产物和依赖目录。
比如:
extension/distnode_modules还是拿前面的例子说:
worker/node_modules 删掉,往往就能直接回收接近 169Mwebsite/node_modules 再删掉,又能回收接近 145M我后来处理磁盘紧张时,基本就是这个思路:不是到处找哪段代码最大,而是先看接下来几天不会碰哪个子项目,然后优先删它的依赖环境。
pnpm 当然也不是完全没有代价。
第一个坑,是老项目可能会依赖一些本来就不该存在的宽松行为。
有些项目以前在 npm 下能跑,不代表它真的写对了。切到 pnpm 以后,如果某个包没显式声明却偷偷在用,往往就会直接报错。
这事烦归烦,但从长期看不算坏事。只是历史债终于被翻出来了。
第二个坑,是少数工具对符号链接支持一般。
现在主流工具链对 pnpm 基本都挺友好,但少数老工具、老脚本、老插件,还是会默认 node_modules 是传统扁平结构。
如果真遇到兼容性问题,pnpm 也不是没留后路。比如可以把 nodeLinker 设成 hoisted,尽量靠近传统结构。官方文档也明确提到,如果工具链对 symlink 支持不好,可以用这种模式。
来源:pnpm Motivation
所以它不是那种“你要么全信,要么别用”的方案。
第三个坑其实是协作问题。
如果团队里有人跑 npm install,有人跑 pnpm install,锁文件和依赖树很容易来回漂。最省心的办法还是统一:
pnpm如果你符合下面这些情况,我基本都会建议试试:
反过来说,如果你只有一个很小的项目,团队工具链也已经完全绑在 npm 或旧版 Yarn 上,那这件事可以先往后放。
回头看,node_modules 占空间这件事,问题往往不在“你写了多少代码”,而在“你养了一整套多重的开发环境”。
删缓存、重装、少装包,这些办法不是没用,只是都偏局部。真正让我体感明显改善的,还是 pnpm 这种从存储模型上减少重复的方案。
它最打动我的地方,不是一句“更快”,而是三件更实际的事:
如果你现在也正被 node_modules 折磨,我会建议别只想着“怎么清掉它”,也顺手想一下:是不是该把包管理器一起换了。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。