Jest、nodemon、ts-node 全删了:Node 内置能力实测,node_modules 少了 380 个目录

🔑 关键词:Node.js, node:test, 类型剥离, node --run, 内置模块

📖 摘要:把 Jest、nodemon、ts-node、dotenv 逐个换成 Node 自带能力的一次实测记录,包含版本要求、具体命令、报错和踩过的坑,以及我最后没换掉的那部分依赖和原因。

我上一个内部项目的 package.json 里,devDependencies 有 41 项,真正跑的业务代码 1700 行左右。在 M1 的 MacBook Air 上 npm ci 冷缓存三分多钟,热缓存也要四十秒——大头全在 Jest 那条依赖链上。这个比例一直让我不太舒服,但活儿忙,拖着没动。

图片

转折是去年秋天 CI 开始偶发超时,查下来是拉某个包的 tarball 时偶尔卡十几秒。那天晚上我开了个分支,一个个试:这东西 Node 自己有没有?

先说清楚,这篇不是「Node 内置模块大合集」,那种文章你随便一搜有几十篇,结构都差不多。我只写自己真替掉的几个,连坑一起写,也说说哪些我到现在都不敢换。

一、node:test 换掉 Jest:node_modules 少了 380 个目录

node --test 其实很早就有了,但 Node 18 那个版本基本不能看,--test-reporter 这些都没有。到 20 后期才算能上生产,t.mock、describe/it、--experimental-test-coverage 陆续补齐。

图片

我的做法是把 jest.config.js 删掉,测试文件继续用 .test.ts,靠 Node 的类型剥离直接跑:

node --test --experimental-test-coverage "src/**/*.test.ts"

那个 glob 一定要加引号。我们本地 Windows 的同事用 PowerShell,不加引号 shell 会先展开,传进去的是一堆乱路径,报错信息还特别不明显;Linux 和 macOS 上不加也未必对。这个坑我替两个人排查过。

代价是什么。expect 那套糖没了,只剩 node:assert/strict,deepStrictEqual 写起来更长,断言失败时的 diff 也不如 Jest 好看。快照功能是后来才加的、还带实验标志,我干脆没用。

图片

最大的摩擦是覆盖率。Jest 默认 Istanbul,Node 用的是 V8 的覆盖数据,两套算法。我们仓库里三个文件的行覆盖数字从 92% 掉到 88%,review 时同事直接问是不是测试写漏了,我解释了半天。数字偏低是统计口径不同,不是退步,但你得挨个跟人说清楚。

结论:纯逻辑的工具函数、API 集成测试,换过去很舒服。前端重度依赖 @testing-library/react 和模块级 mock 的项目,别碰,省下的那点安装时间抵不过重学一套断言 API 的时间。

二、类型剥离能跑,不等于能用

Node 22.6 加了 --experimental-strip-types,22.18 之后默认打开(23.6 也默认),意思就是 node index.ts 直接能跑,tsx 和 ts-node 可以从 devDependencies 里划掉了。

图片

但注意三个字:只剥离。它把类型注释删掉就交给 V8 跑,一行类型都不检查。所以 tsc --noEmit 还是得留在 CI 里,该跑跑,这两件事是分开的。

不支持的东西也很明确:enum、namespace、构造函数参数属性,一写就甩 unsupported TypeScript syntax 的错。tsconfig 里的 paths 别名它完全不认,@/utils 这种导入该报错还是报错。我们有几个老文件就是因为用了 enum,一直没敢迁。

还有个很实际的坑:你本地 Node 24 跑得好好的,生产镜像里要还是 node:20,node app.ts 直接给 ERR_UNKNOWN_FILE_EXTENSION。动这个之前先确认部署环境的最低版本——尤其用 Vercel、Lambda 这类平台的时候,package.json 里的 engines 基本约束不了什么,平台给你哪个版本你才用哪个。

三、watch、env-file、--run:省的都是零碎,攒起来不少

图片

node --watch(22 起算正式)替 nodemon,行为没那么花哨,但只监听你 import 进去的文件,不会像 nodemon 那样把整个目录扫一遍。我们一个 monorepo 里跑 nodemon 内存能飙到 1G 出头,换了之后没再见过。

--env-file=.env 从 20.6 开始有,22.9 加了 --env-file-if-exists。它不支持变量插值也不支持多行值,写法比 dotenv 死板,好在我们 .env 本来就简单,够用。真正的好处是不用再写 import 'dotenv/config' 那一行,加载顺序的问题也一并没了。

node --run test 代替 npm run test,省掉 npm 自身的启动开销。我粗测单次差一百多毫秒到几百毫秒——样本太少,别当基准测试看。但脚本一多、CI 一天跑几十次,这笔账是正的。它不跑 pre/post 钩子,这个反倒帮了我:另一个仓库有个 pre 钩子会在每次启动前重建一遍类型,慢得要死,过去一直没人敢删。

四、有点反直觉:我最后留住的东西

图片

替代到最后,收益最大的其实不是启动速度,是 npm audit 的噪音。41 个 devDependencies 砍到 7 个以后,漏洞告警从一屏变成三条,而且每条都能看懂。

但我留住了 zod、drizzle、fastify 这一类。内置能力替换的是胶水——读环境变量、监听文件、跑测试、认 TypeScript——不是领域能力。任何一个超过两百行的校验库,你自己写一版只会更差。

还有个更隐蔽的成本:用内置功能其实是在对运行时下注。依赖可以锁版本,运行时锁不了。你今天写爽了 node:sqlite 的 DatabaseSync,明年要往一个只支持 Node 20 的环境上搬,那就是一整轮返工。

所以. 我现在的做法是:新项目直接用 Node 24 起步,内置能干的全用内置,测试先写 node:test;老项目只动 watch 和 env-file 这两块,风险最低,收益也最快看得见。写到这里差不多了,剩下的坑等我再踩几个再说。

🏷️ 标签: