ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

2026年值得删除的5个npm包:用原生API实现依赖瘦身

2026年值得删除的5个npm包:用原生API实现依赖瘦身 说实话这两年每次做依赖升级我最常干的一件事就是从 package.json 里成批删除 npm 包。不是项目变简单了而是运行环境和原生 API 变得太强了很多当年必须引第三方库才能解决的事如今几行原生代码就能搞定。2026 年再回头看有一批曾经人手一个的 npm 包已经明显冗余留着纯粹是增加安装时间、占体积、加供应链风险。这篇文章我会从实际维护者的视角把 5 个最典型的“可删 npm 包”逐一拆开讲清楚包括它当年为什么要存在、现在用什么替代、迁移过程需要注意什么以及哪些场景下你反而应该继续留着它。谁来读这篇文章凡是维护前端工程、Node.js 服务、全栈应用同时还在用这些老牌工具库的开发者和技术负责人都应该抽出半小时做一次依赖体检。我也见过很多团队“从接手项目起就没动过这些祖传依赖”每次 npm install 都在为历史包袱买单。下面这 5 个包2026 年大多数项目里真的可以删了。1. 2026 年为什么这些包该退休了1.1 运行时能力这几年的变化过去十几年前端依赖的核心逻辑一直是“浏览器和 Node 做得太少社区就补什么”。jQuery 补 DOM 操作的坑lodash 补数组对象方法的坑axios 补请求管理的坑moment 补日期处理的坑rimraf 补跨平台删除的坑dotenv 补环境变量读取的坑。本质上都是因为原生 API 太弱或者不统一。但 2020 年之后这个逻辑正在翻盘。浏览器端的 fetch、AbortController、structuredClone、可选链、空值合并、ES2023 新增的一批不可变数组方法Node.js 端从 14 到 22 的快速迭代把文件删除、环境变量加载、原生测试工具、内置 .env 支持全部补齐了。到了 2026 年主流运行时已经覆盖了这些包 80% 以上日常使用场景。我自己的经验是真正留在代码里的老包函数调用往往只是那几个最基础的 API根本配不上一个完整 npm 包的存在。1.2 判断依赖是否冗余的三个标准我建议所有团队在删包之前先拿这三个标准过一遍避免拍脑袋决定功能是否已被原生 API 覆盖打开代码库搜一下这个包用到的函数数量多不多。比如 lodash 用了几十个方法那说明业务确实重度依赖它如果只用三五个比如_.get、_.uniq直接用原生替代几乎没有成本。包是否还在积极维护moment 官方在 2020 年就宣布进入维护模式只修 bug 不加功能了。一个不再演进的基础库在 2026 年继续使用意味着你每年都在累积技术债。体积和风险的性价比axios 压缩后大概 13KB 左右moment 更是 300KB 级别的庞然大物。每次安装都要经历依赖解析、下载、构建还都处在你的供应链攻击面上。能删一个就少一份风险。三个标准只要中两条这个包就可以进入“待删除名单”了。下面具体聊五个我实测删掉之后项目依然稳定运行的包。2. 五个可以删除的 npm 包逐个拆解2.1 lodash原生数组方法已经够用lodash 可能是最典型的“拳打南山敬老院”型工具库。当年它的价值在于统一浏览器差异、补齐 ES5 时代缺失的数组方法、提供链式调用和深拷贝这类高级工具。但 ES6 到 ES2023 这么多年迭代下来原生数组方法已经基本齐全。我梳理过一个普通后台管理系统里 lodash 的使用情况发现排名前几的调用几乎都是这些_.map、_.filter、_.find、_.includes、_.uniq、_.get、_.cloneDeep。而这些在 2026 年的 JavaScript 里全都有原生替代方案// 过去 _.includes(arr, foo); _.uniq(arr); _.get(obj, a.b.c, default); // 现在 arr.includes(foo); [...new Set(arr)]; obj?.a?.b?.c ?? default;最容易被忽略的一点是 ES2023 给数组加的四个方法toSorted、toReversed、toSpliced、with。以前用 lodash 是因为它不会修改原数组比如_.sortBy、_.reverse都很友好而原生Array.prototype.sort会原地修改数组。现在原生toSorted()也能做到不改原数组了最后一块拼图也补齐了。深拷贝方面以前只能_.cloneDeep(obj)现在浏览器和 Node 都内置了structuredClone(obj)连Date、Map、Set、ArrayBuffer都能正确拷贝。// 过去 const copy _.cloneDeep(complexObj); // 现在 const copy structuredClone(complexObj);那_.debounce和_.throttle呢2026 年其实有更好的选择——直接用AbortSignal.timeout或者自己封装一个十几行的防抖函数。防抖节流本质是“定时器 闭包”自己写个useDebouncehook 或工具函数也就十几行代码比引整个 lodash 划算太多。注意一旦决定删 lodash别靠猜测写代码。推荐的方式是先全局搜索_.列出所有用到的函数然后逐个对照原生方案确认没有遗漏再动手。2.2 axiosfetch 和 AbortController 已经成熟axios 是我删掉后最“无感”的一个包。当年选它主要因为三个理由请求拦截器和响应拦截器、请求取消、统一错误处理。这几个需求在 2026 年用原生 fetch 都能解决而且不会引入一个 13KB 的依赖。先说请求取消。fetch 配合 AbortController 早就支持了现在更简单// 过去 const source axios.CancelToken.source(); axios.get(/api/data, { cancelToken: source.token }); // 现在 const controller new AbortController(); fetch(/api/data, { signal: controller.signal });如果只是想要超时取消一行代码搞定fetch(/api/data, { signal: AbortSignal.timeout(5000) });再说拦截器。axios 拦截器本质上就是“请求发出前统一改配置 / 响应回来前统一处理数据”。用原生 fetch 只需要封装一个request函数async function request(url, options {}) { // 请求拦截 const token localStorage.getItem(token); const headers { Content-Type: application/json, ...(token ? { Authorization: Bearer ${token} } : {}), ...options.headers, }; const res await fetch(url, { ...options, headers }); if (!res.ok) { // 统一错误处理 if (res.status 401) { location.href /login; } throw new Error(HTTP ${res.status}: ${res.statusText}); } return res.json(); }不过这里有个关键差异必须说清楚fetch 在 HTTP 状态码为 4xx/5xx 时并不会 reject它只在网络层失败时才 reject。使用 axios 的时候catch能捕获 404、500 这类响应换了 fetch 之后你必须显式检查response.ok或者res.status否则错误响应会悄悄被当作成功走完流程。上面那段封装里我特意加了一行if (!res.ok)就是干这个用的。那么什么情况下不建议删 axios如果你的项目重度依赖请求上传进度比如大文件上传场景需要onUploadProgress这一点原生 fetch 至今没有提供标准事件而 axios 底层用的是 XHR天然支持。这种情况可以保留 axios或者改用 fetch 分片上传的方案。但常规的 JSON 请求2026 年的 fetch 完全够用。2.3 dotenvNode.js 原生支持环境变量文件dotenv 解决的事情非常单一把.env文件里的键值对加载到process.env。但这件事从 Node.js 20.6 开始原生就支持了——你只需要在启动命令里加一个--env-file参数# 过去 node -r dotenv/config server.js # 现在 node --env-file.env server.js更进一步Node.js 22 还提供了--env-file-if-missing允许.env文件不存在时不报错。这在本地开发和 CI 环境非常有用node --env-file-if-missing.env server.js删掉 dotenv 后package.json 的 scripts 改一下就行{ scripts: { dev: node --env-file.env --watch server.js, start: node --env-file.env server.js } }代码里也不再需要require(dotenv).config()这一行了。项目里所有process.env的读取逻辑保持不变。这里有个容易踩的坑dotenv 的解析规则和 Node 原生的--env-file在一些细节上不完全一致。比如 dotenv 支持多行值、支持export前缀、对引号和转义的处理也更宽容Node 原生加载器刚开始对单引号、双引号和特殊字符的处理比较严格。我的建议是迁移后立刻跑一遍本地和 CI 的启动流程重点检查配置里包含空格、#、特殊符号的环境变量确认加载结果没变化再删除依赖。我个人体会是dotenv 属于“删起来最不起眼但收益很实在”的改动。它体积不大但少一层依赖解析少一个包Node 启动参数还更标准了。2.4 rimraffs.rm 一行的活儿rimraf 曾经是 Node 生态里删除目录的标配。原因很简单也很无奈Windows 和 Unix 的rm -rf行为不一样跨平台删除在 Node 里一直很麻烦所以社区专门做了一个包来抹平差异。但 Node.js 14.14 之后fs.rmSync和fs.promises.rm就支持递归删除了。到 2026 年还会有人在代码里专门引入 rimraf 吗不值得。过去常见的用法是给脚本配置{ scripts: { clean: rimraf dist coverage node_modules/.cache } }现在直接写{ scripts: { clean: node -e \require(fs).rmSync(dist,{recursive:true,force:true}); require(fs).rmSync(coverage,{recursive:true,force:true});\ } }不过这条命令写长了确实丑我一般更推荐在项目根目录放一个scripts/clean.mjs// scripts/clean.mjs import { rmSync } from node:fs; const targets [dist, coverage, node_modules/.cache]; for (const target of targets) { rmSync(target, { recursive: true, force: true }); }然后 package.json 里就干净了{ scripts: { clean: node scripts/clean.mjs } }rmSync的参数recursive: true表示递归删除force: true表示目标不存在也不抛错这就是 rimraf 默认行为的完整替代。我自己在多个全栈项目里这样替换过构建流程没有任何异常。唯一要留个心眼的是rimraf 对一些边界情况处理得更“暴力”比如 Windows 上遇到只读文件、路径过长、符号链接循环时会更健壮。如果你的 clean 脚本经常要删除特殊的临时目录比如恶意软件测试目录、特殊符号目录可以短期保留 rimraf。但对大多数应用项目来说fs.rmSync已经完全够用。2.5 momentTemporal 和 Intl 正在接管日期领域moment 是 2026 年最该删但没有删的一个很多项目里它已经成了“祖传代码”没人敢动。现在不用纠结了浏览器和 Node 对日期处理的能力已经全面提升连一向难搞的时区问题也有了标准答案。格式化日期用Intl.DateTimeFormat。假如你要输出2026-05-18这种格式// 过去 moment().format(YYYY-MM-DD); // 现在 new Intl.DateTimeFormat(sv-SE, { year: numeric, month: 2-digit, day: 2-digit, }).format(new Date()); // 2026-05-18对相对时间“3 天前”用Intl.RelativeTimeFormatconst rtf new Intl.RelativeTimeFormat(zh-CN, { numeric: auto }); rtf.format(-3, day); // 3天前时区转换用Intl.DateTimeFormat的timeZone选项// 过去 moment.tz(2026-05-18 10:00, Asia/Tokyo).format(); // 现在 new Intl.DateTimeFormat(zh-CN, { timeZone: Asia/Tokyo, year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit, second: 2-digit, }).format(new Date(2026-05-18T01:00:00Z));更值得关注的是 TC39 标准的 Temporal API。到了 2026 年它在主流运行时里的可用性已经相当高了。Temporal 提供了Temporal.Now、Temporal.PlainDate、Temporal.Duration等现代日期时间原语彻底解决了老 Date 对象月份从 0 开始、不可变缺失、时区混乱等历史问题// 获取当前日期 const today Temporal.Now.plainDateISO(); // 输出格式2026-05-18 // 加一天不会修改原对象 const tomorrow today.add({ days: 1 }); // 计算两个日期的间隔 const duration Temporal.PlainDate.from(2026-01-01).until(today); console.log(duration.days);如果你所在项目的运行时环境还无法使用 Temporal那建议先把 moment 替换成 dayjs至少包体积能从 300KB 降到几 KBAPI 风格还几乎不用改。但我个人更推荐一步到位到Intl原生方案因为日常业务用到的日期操作无非格式化、解析、加减、比较、时区这几个场景Intl都能覆盖根本不需要再引第三方包。3. 实操安全移除依赖的完整流程3.1 清理前的依赖体检删包不是直接npm uninstall那么简单盲目删除可能把生产环境搞挂。我建议按下面这个顺序做一次体检列出依赖清单把dependencies和devDependencies里所有包名拉出来先圈定本文提到的这 5 个包以及同类已经停止维护的包。统计调用点全局搜索包名或简称比如_.、axios.、moment(统计出现次数和文件分布。调用次数越少迁移成本越低。确认替代方案对每个调用点判断能否用原生 API 直接替换。不能直接用原生 API 的再评估是否是极端场景。确定版本基线删包前记录当前 Node 版本和浏览器兼容性要求。如果你的产品还要兼容某个 2018 年的老旧浏览器内核那部分替换方案可能不适用。3.2 逐包迁移与验证我的建议是一个包一个包改改完立刻跑测试不要五个包一起动。出现问题时你能立刻定位是哪个包引起的。以 axios 换成 fetch 为例实际操作顺序大致是新建一个request.js或api.js工具模块把 fetch 封装成统一入口。在封装的函数内部实现请求头注入、超时设置、错误响应判断、401 跳转。逐个替换页面或模块里的axios.get、axios.post调用把原来的.then回调改为await request(...)。跑单测和端到端测试重点验证所有接口的错误分支。全部替换完成后在 package.json 中删除 axios执行npm uninstall axios。这类替换最忌讳的是“改一半留一半”——出现有些请求走 axios、有些走 fetch 的混乱状态后续维护的人会更痛苦。宁可一次改完所有页面再回归测试也不要留尾巴。3.3 删除依赖后的检查清单删除 npm 包之后我一般会做这几件事更新 package-lock/yarn.lock/pnpm-lock确保 lockfile 里没有残留引用。npm install后跑一遍构建确认没有因为代码里忘了删的引用而报错。CI 全量跑一遍测试和 lint。很多 package 删掉后eslint和typescript会立刻报错这时就是清理死代码的最好机会。手动验证核心用户路径尤其是登录、支付、文件上传这类强依赖网络或配置的环节。在 changelog 或 PR 描述里写明删除了哪些依赖方便团队成员 review。还有一个容易被忽略的如果你用的 monorepo多个子包可能同时引用了同一个工具库。删之前先确认所有子包都没有再使用或者做统一替换避免只清了其中一个。4. 常见问题与避坑指南4.1 替换 lodash 时的类型边界问题_.get换成?.和??之后有一个常见差异_.get对属性路径的支持和容错性更强比如_.get(obj, a[0].b)这种写法原生?.不支持数组下标语法。如果你真的依赖这种字符串路径访问建议先改用函数式写法// 需要一个安全访问函数而不是直接上 ?. const safeGet (obj, key) key in obj ? obj[key] : undefined; const path a.b.c; const value path.split(.).reduce(safeGet, obj) ?? default;另外structuredClone也不是兼容所有对象函数、原型对象、DOM 节点都会抛错这一点和_.cloneDeep的行为不同。如果业务里要深拷贝的东西是 class 实例或函数别硬换单独写个浅拷贝足够。4.2 fetch 的错误处理和不带 progress 的问题我在 2.2 里提过 fetch 不会对 4xx/5xx 主动抛错这是迁移时最常见的 bug这里再展开一下。很多团队删除 axios 后发现原来在catch里统一弹错的逻辑全失效了因为 fetch 把这个责任交给了业务代码。解决方案就是在自己封装的request函数里统一判断res.ok并且补上网络错误、超时错误、JSON 解析错误三类 catch不要让错误静默吞掉。另一个容易漏的点是fetch默认不带 cookie。axios 默认withCredentials: falsefetch 默认credentials: same-origin。如果你要发跨域请求并携带用户凭证必须显式设置fetch(url, { credentials: include, });4.3 dotenv 与 --env-file 的解析差异Node 原生--env-file和 dotenv 在.env文件解析上的差异主要出现在下面几种情况dotenv 支持.env里写多行引号字符串原生加载器对跨行值的支持曾经有限升级到 Node 22 后仍建议重点验证。dotenv 允许变量在.env中重复定义默认最后一条生效Node 原生加载器对重复键的行为也可能不同。.env文件里如果包含#注释、export关键字等两边处理不完全一致。实际操作中我建议迁移后直接在本地打印一遍所有关键环境变量逐项比对别用肉眼在.env文件里扫一遍就完事。4.4 团队协作与兼容性检查删包这件事表面上只是技术动作但它是典型的“牵一发动全身”改动。我见过一个团队把 axios 换成 fetch 后后端同事因为读不到自定义 header 而排查了两天最后发现是 fetch 默认不允许跨域暴露某些响应头需要后端配合加Access-Control-Allow-Headers。这种事一定要提前和后端打招呼而不是悄悄改完就推上去。另外如果你负责的项目是需要分发给第三方使用的 npm 包删依赖会更谨慎。npm 包发布后消费者可能运行在和你完全不同的环境里。建议在发布 npm 包前明确声明 Node 版本要求和浏览器兼容性基线否则用了原生 API 后消费者的旧环境会直接报错。实际上我在发布和维护开源 npm 包时现在都会把依赖项降到最低能用引擎特性解决的绝不引包这对使用者是好事也是自己维护成本的大幅下降。最后分享一点实际经验做依赖清理这件事最怕的不是技术难度而是“大家都在用所以我不敢动”的心理。我在好几个项目里都发现所谓“必须用 axios 的拦截器”“必须用 lodash 的深拷贝”等真的动手去翻代码时绝大部分调用点都是最简单的那几个 API换原生方案半天就改完了。真正要保留下来的反而是那些底层依赖很强、生态绑定很深的包——比如 React、Vue、Express 这类框架本身以及你业务里核心的组件库和工具链。工具库层面的过度依赖2026 年真的可以放开手去删。建议每个团队在建项目或者做技术周会时把“依赖体检”当成一个常规事项每半年拉一次依赖清单对着原生 API 的变化更新一遍看看哪些包已经过时。这个习惯坚持下去你的 node_modules 体积、安装时间、供应链风险和代码可维护性都会在不知不觉中变得健康很多。
返回列表