这话一说出口,可能很多前端同学会立刻笑出来:"当然被玩坏了!"但咱别急着笑,我先把话摊开讲一讲。很多人每天打开项目就像进了仓库------成百上千个 node_modules、动辄几十来个依赖,一个功能装一堆包,结果最后项目臃肿得像个大肉包。你可能会想,"我就要用这个小功能,装个包不行吗?"当然行,可问题是,技术本来是为人服务的,不是让我们做它的奴隶。
前几年我们搞请求就装 axios、搞日期就装 moment、深拷贝装 lodash、UUID 装 uuid、WebSocket 装 ws,反正遇到点儿啥先去包管理器搜一圈。结果现在你打开 Node.js 官方文档一看,Node 自己就带了好多以前靠包才能搞定的功能:原生 fetch() 现在可以替代 axios,crypto.randomUUID() 可以替代 uuid,而 fs/promises 的 glob() 可以替代 glob 包了。甚至 Node 从 18 版本开始就把浏览器里的 fetch() API 作为全局函数支持了,这意味着你 根本不需要外部的 HTTP 请求库 来写大多数网络请求代码了。

这听起来是不是有点魔幻?我给你一个真实例子:
以前在一个团队里,我们项目里到处用 axios 做请求,因为几年前 Node 的 fetch() 还不稳定,也不普遍。后来团队升级 Node 到 18 以上,我们试着把请求逻辑从 axios 移到原生 fetch()。以前的代码是这样:
javascript
import axios from 'axios';
const res = await axios.get('https://api.example.com/data');
console.log(res.data);
改成原生后就变成:
ini
const res = await fetch('https://api.example.com/data');
const data = await res.json();
console.log(data);
是不是简洁得多?而且不用再去关注 axios 的版本升级、漏洞问题、依赖冲突啥的了。
再举一个例子,项目里你要生成一个 UUID,传统写法常常是这么写:
javascript
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
而现在最新 Node 自带的 crypto 直接就能搞定:
ini
import { randomUUID } from 'node:crypto';
const id = randomUUID();
结果库体积减少了,而且不再需要去更新第三方包。

光这一点就够痛快了:别总想靠包解决一切问题,有的时候其实内置已经够用了。但问题是,社区心态还停留在从前。npm 生态太丰富,导致一个小功能前前后后会有好几个包,你看着别人用,就不自觉装上。这就造成了一个"依赖恐惧症"------别人装了,我不装好像不专业一样。

你可能没意识到的生态债
说到这里,我们再聊聊技术债。技术债不是一个专有名词,它指的就是"因为选择不够优化,后来不得不付更高代价去修复或重构"的那部分成本。在前端里,这种债往往藏在项目里无数 npm install 后的依赖树里。你每装一个包,就可能带来几十个依赖,这些依赖里有未更新的、有漏洞的,也有你根本用不到却被间接引入的。你可能一个月没关注它,但 GitHub 的 Dependabot 会给你提示"你的依赖又过期了",或者 CI/CD 在某次版本升级之后突然挂掉。
举个我亲身经历的例子:我维护过一个大型前端项目(React + Node 服务端渲染),package.json 里有近 70 个依赖。深究之后发现,其中 30% 的包其实是"只用了其中一个函数"或者"早就有原生 API 可替代"的,比如用于深拷贝的 lodash、处理日期的 moment、处理 URL 参数的 qs。而这三者都可以用现代 JavaScript 自带的语法或 API 实现,或者替换成更轻量的库,也能显著减小打包体积、提高启动速度。
比如 lodash 的深拷贝:
javascript
import cloneDeep from 'lodash/cloneDeep';
现代 JS 有 structuredClone(浏览器和 Node.js 都支持),可以轻松替代:
ini
const cloneObj = structuredClone(originalObj);
这段代码不仅更短,而且可读性更强、依赖更少。AgedCoffee
又比如日期处理很多人习惯用旧时代的 moment.js,但它本身已经不推荐使用,因为它体积大、功能冗余,而且 JavaScript 现在有更现代的日期处理方式,比如 Date.prototype.toLocaleString() 和现代库 date-fns、dayjs(如果非要用库的话都比 moment 轻得多)。 资料来源携程技术
为什么我们还装那么多包?
你可能会问,"Ok,我知道原生 API 现在很强,但为什么社区里还是大量依赖包?大家不跟进 Node 的变化吗?"这其实是一个心理层面的问题。
一:习惯性依赖 。很多时候写代码不是为了技术最优,而是为了速度。你在 2018 年学会了 axios、lodash、moment,那时候它们确实是最方便的解决方案。当时好用就装,现在不卸也是习惯。技术 Debt 最大的问题往往不是技术本身,而是我们不去主动清理以前的选择。
二:包生态太丰富。npm 上 100 万+ 包意味着几乎每个问题都有现成的方案,但这也意味着很多包功能高度重复,选择成本高。你可能花更多时间去对比包,而不是思考"我真的需要它吗"。这导致了一个怪圈:你想着省事儿,结果反而装了更多包。
三:团队沟通成本。在多人项目里,有人写代码时引用了某个包,别人可能不知道这个包的替代方案,导致后来大家都默认继续使用。一旦团队形成了某一套依赖体系,想要把冗余依赖剔除就变得麻烦。
如何优雅地管理依赖,而不是被它们"玩坏"
既然生态是大趋势,我们也没必要否定 npm 的价值。但我的经验是,你可以用一种**"最小依赖原则"**来管理你的项目:
- 先用原生 :先看看 Node 或浏览器原生能不能满足需求。HTTP 请求现在就自带
fetch();唯一要注意的是一些高级特性(比如请求拦截、重试逻辑)可能仍需要库辅助。 - 评估替代方案 :如果原生不够,再找轻量替代,而不是默认用你最熟悉的大包。比如用
structuredClone()替代lodash.cloneDeep,用URLSearchParams处理查询参数。 - 按需引入:如果确实要用大库,尽量按需引入。例如 lodash 的单函数模块,而不是整个库。
- 做好定期审查:每隔一段时间,审查项目依赖,看看哪些依赖可以清理或替换成原生方案。
总结
我们不是在否定 npm 或生态的价值。npm 的存在对整个 JavaScript 社区来说是革命性的,它让我们不用重复造轮子、快速构建应用。只是随着 Node.js 原生功能越来越强、浏览器标准越来越完善,很多你"习惯性"装的包已经有原生替代方案了,继续装就像背着枷锁前进一样。经过实践我发现------依赖越少,项目越轻盈,维护越省心,CI/CD 越稳定。技术债是隐性的,早清理早轻松。给项目留一条回头路,而不是越走越难走的依赖迷宫,这才是真正的前端成长。