2026年node_modules减肥指南:5个可删依赖包及原生替代方案

1. 为什么 2026 年反而该给 node_modules 减肥了

先说明一下,我不是反对用第三方包。npm 生态是前端能够走到今天的关键,没有这两百多万个包,我们做不了这么多事。但这两年我越来越明显感受到一个趋势:浏览器和 Node.js 原生能力在快速补齐,很多当年“不得不装”的包,现在已经是纯粹的负担了。

我说的负担,不是装完占几百 MB 硬盘这么简单。每个依赖都意味着安装时间变长、依赖树变复杂、安全审计面变大、供应链风险增加。你知道现在 npm 上最头疼的事情是什么吗?不是包不够用,而是你根本不知道你装的这个包的间接依赖里藏了什么。一个npm install下去,锁文件里多出四五百个包是常态,里面可能有一半你听都没听说过。每次npm audit扫出一堆漏洞,修都修不完,问题往往出在那些你根本没用到的间接依赖上。

我给团队定过一个规矩:新增一个依赖,必须写清楚它的不可替代性。如果一句话说不清楚,那就不许加。别笑,实际执行下来大家发现,很多包其实早就有原生替代方案了,特别是下面这几个。

还有一个驱动因素是从 Node 14 到 Node 22 以及浏览器平台的持续迭代。原生 API 已经覆盖了我们日常开发中的绝大多数场景——日期处理、HTTP 请求、数组操作、文件删除、环境变量读取……而且原生 API 往往是 V8 直接实现的,性能和内存占用反而比引入一个包要好。JS 引擎的团队这些年在标准化上面花的力气是实打实的,我们这些使用者如果不跟进,就会被时代悄悄留在原地。

这篇文章里我挑出 5 个我实测可以在 2026 年从项目里删掉的包,每个都给出替代方案、迁移思路和踩坑记录。注意,我说的是“可以删掉”,不是“必须删掉”——如果你的代码库里已经用得很顺手,历史包袱也重,那不强求立刻动刀。但如果是在开新项目,或者正在做依赖治理,下面这 5 个包值得你认真看一遍。

2. 第一个可以删掉的包:dayjs 这样的日期处理库

2.1 dayjs 曾经解决什么问题

几年前我们用 dayjs 或 moment.js,最核心的痛点是:原生Date对象太不好用了。解析字符串要碰运气,格式化要靠手写拼凑,时区处理直接就是灾难。做国际化产品的时候,同一个时间在不同地区要显示成不同的文本,原生 API 完全帮不上忙。

moment.js 现在基本已经退场了,dayjs 成了最主流的替代品,体积小、API 友好、插件机制完善。我自己的很多老项目里也还在用 dayjs,说实话它确实好用,团队没人抱怨过。

但问题在于:我们用 dayjs 解决的三个核心问题——格式化、相对时间、时区处理——原生 API 在 2026 年全都已经有成熟方案了。

2.2 原生 API 已经做到什么程度

先说最基础的格式化。Intl.DateTimeFormat已经存在很久了,但早期用起来确实麻烦。后来Date.prototype.toLocaleDateString和toLocaleString的 API 越来越完善,配合options参数已经能覆盖绝大多数业务场景:

// 以前写 dayjs 需要做的事情 const d = dayjs("2026-03-15T18:30:00"); d.format("YYYY年M月D日 HH:mm"); // "2026年3月15日 18:30" // 原生方案 const date = new Date("2026-03-15T18:30:00"); new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false }).format(date); // "2026年3月15日 18:30"

再说相对时间。以前要显示“3 天前”“2 小时前”,需要自己写逻辑或者借助 dayjs 的 relativeTime 插件。现在Intl.RelativeTimeFormat原生支持:

const rtf = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto" }); rtf.format(-3, "day"); // "3天前" rtf.format(2, "hour"); // "2小时后"

最后是时区。这个是 dayjs 的 timezone 插件最核心的功能,原生方案是Intl.DateTimeFormat的timeZone参数,以及Date.prototype.toLocaleString的timeZone选项。2024 年之后,TemporalAPI 的提案逐步落地,虽然到 2026 年还没有在所有运行时默认开启,但已经可以在多数主流浏览器和 Node.js 22+ 的 flag 下使用了。Temporal 把日期、时间、时区、持续时间这些概念完全分开,设计上比 JavaScript 的Date不知道高到哪里去了。

注意:Temporal 到 2026 年仍然建议只在实验性项目里使用,生产环境优先考虑Intl系列 API,它们已经足够成熟稳定。

2.3 迁移方案和坑

我的建议是分三步走:

  1. 把项目里所有dayjs().format(...)调用列出来,逐个用Intl.DateTimeFormat替换。多数情况下只需封装一个formatDate(date, pattern)工具函数。
  2. 把相对时间逻辑替换为Intl.RelativeTimeFormat。这个 API 的浏览器兼容性在 2026 年已经非常好,Node.js 侧也完全没有压力。
  3. 最后把 dayjs 从package.json里删掉,跑一遍全量测试,重点看快照测试里的时间格式是否正常。

踩过的坑主要有两个:

  • Intl.DateTimeFormat的月份和星期几默认是英文的,一定要传locale参数,并且根据业务需要配置era、year、month、day、weekday等字段。
  • 如果你用了dayjs().add(1, "day")这类链式操作,原生方案没有直接等价物。需要自己封装:
function addDays(date, days) { const result = new Date(date); result.setDate(result.getDate() + days); return result; }

这不算复杂,但确实需要你花半天到一天时间去适配。

3. 第二个可以删掉的包:axios 这样的 HTTP 请求库

3.1 axios 的历史地位和现状

axios 可以说是前端最知名的网络请求库,它的核心优势在于请求/响应拦截器、取消请求、超时控制、自动 JSON 转换和浏览器环境适配。在 fetch 还没普及的年代,XMLHttpRequest的 API 设计太反人类,axios 把那一层封装得足够好用,所以它成为了事实标准。

但现在我们得认真看一下:原生fetch已经是所有现代浏览器和 Node.js 全局支持的标准 API。2024 年和 2025 年,Node.js 的 fetch 实现(基于 undici)已经非常稳定,之前所谓“Node 18 的 fetch 还不靠谱”“上传文件有 bug”这些问题基本都解决了。到 2026 年,我至少在自己维护的十几个项目里都用纯 fetch 跑过生产流量,没有发现任何一例由于 fetch 本身导致的稳定性问题。

3.2 fetch 如何覆盖 axios 的常用能力

先用一张表来对比,这样比较直观:

功能axios原生 fetch 方案
基础请求axios.get(url)fetch(url)
请求参数params: { id: 1 }URLSearchParams或手动拼 URL
超时控制timeout: 5000AbortSignal.timeout(5000)
取消请求CancelTokenAbortController
拦截器请求/响应拦截封装通用request()函数
自动 JSON 转换默认行为json()方法
错误处理err.responsetry/catch中判断response.ok
上传进度onUploadProgressfetch暂无原生成熟方案,需用XMLHttpRequest或流式上传

核心要点是拦截器。axios 的拦截器确实是好用,但说白了就是一个洋葱模型,用原生 fetch 也能很容易地实现同样效果:

async function request(url, options = {}) { // 请求拦截 if (options.token) { options.headers = { ...options.headers, Authorization: `Bearer ${options.token}` }; } // 超时处理 const timeout = options.timeout ?? 10000; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); // 响应拦截:统一处理错误码 if (!response.ok) { const error = new Error(`HTTP ${response.status}`); error.status = response.status; throw error; } const data = await response.json(); return data; } finally { clearTimeout(timer); } }

3.3 哪些场景下暂时还不能完全抛弃 axios

我在实际迁移中也发现一个场景,fetch 目前确实不如 axios 方便:上传文件并监听进度。fetch 的原生Request和Response对象都是基于 stream 设计的,理论上可以监听底层流的进度,但实现起来非常繁琐,而且并不标准。不过在实际业务里,文件上传场景我们已经有更好的选择——XMLHttpRequest的upload.onprogress是最成熟可靠的方案,或者直接使用二进制流加一个ReadableStream包一层。我自己一般会保留一个基于XMLHttpRequest的上传工具函数,其它场景全部换成 fetch。

另外一个场景是超时和重试的组合逻辑。axios 上有axios-retry这样的库,原生 fetch 没有直接等价物。但这个问题也不难解决,自己写一个约五十行的重试工具就好,并不复杂。

4. 第三个可以删掉的包:lodash 这样的工具函数库

4.1 lodash 的定位和实际使用率

lodash 是 npm 下载量最高的包之一,但如果你仔细审视自己的代码,会发现你真正用到的函数可能不超过十个:_.debounce、_.throttle、_.cloneDeep、_.groupBy、_.isEqual、_.get、_.set……这些函数确实帮我们省了不少事。

但问题在于,从 ES6 开始,JavaScript 原生就一直在补齐数组、对象、字符串的处理能力。到了 ES2023,新增了Array.prototype.toSorted、toReversed、with、findLast等一批数组方法;ES2024 又增加了Object.groupBy和Map.groupBy;再加上已有的Object.fromEntries、Object.hasOwn、Array.flatMap、Array.at等等。到 2026 年回头看,lodash 最核心的那十几个工具函数,原生 API 已经能覆盖掉至少八成以上。

4.2 一些具体替换映射

撬开核心的几个看:

// _.get 可以用可选链 + 空值合并运算符替代 const value = _.get(obj, "a.b.c", "default"); // 原生 const value = obj?.a?.b?.c ?? "default"; // _.groupBy 可以用 Object.groupBy const grouped = _.groupBy(users, "age"); // 原生 const grouped = Object.groupBy(users, user => user.age); // _.cloneDeep 可以用 structuredClone const clone = _.cloneDeep(deepObject); // 原生 const clone = structuredClone(deepObject); // _.debounce / _.throttle 在多数场景下可以用简单封装替代 function debounce(fn, delay) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }

这里重点说一下structuredClone。它是 2022 年进入标准的全局函数,底层用 HTML Structured Clone Algorithm 实现,可以深拷贝绝大多数 JavaScript 对象,包括Date、Map、Set、RegExp、ArrayBuffer、TypedArray等,而且性能表现远好于 JSON 序列化或者手动递归。2026 年的 Node.js 和浏览器都已经默认支持。我在迁移 lodash 时用structuredClone替换掉了所有_.cloneDeep,实测下来没有问题。

另一个容易被忽略的是Object.hasOwn。以前判断一个对象是否有某个自有属性,要用Object.prototype.hasOwnProperty.call(obj, key),特别啰嗦。lodash 的_.has也有类似的坑。现在直接用Object.hasOwn(obj, key)就好,原生支持不需要任何填充。

4.3 lodash 哪些部分仍然值得保留

说实话,lodash 有一些函数原生至今没有等价物:

  • _.merge:递归合并多个对象,且能正确处理数组、嵌套对象。原生Object.assign和展开语法都是浅拷贝,实现深度合并仍然需要自己写或者借助其它小工具包。
  • _.pick/_.omit:选取或排除对象属性的工具函数,原生没有直接等价物,但用解构加 rest 语法也能实现大部分场景。
  • _.uniqBy/_.sortedUniq:在某些特定数据结构上确实好用。

我的建议是:不要一次性把 lodash 全删掉。先把不需要的部分拆出去,用原生替代。最后如果只剩少数几个函数,直接复制它们的功能到项目里(代码量很小),或者用 lodash 的按函数引入(lodash/merge这种路径)方式,而不是全部引入。

5. 第四个可以删掉的包:rimraf 这样的删除文件工具

5.1 rimraf 的历史背景

rimraf 这个包有多经典?它是对rm -rf的跨平台实现。在 Windows 和 macOS 上,直接用 Node.js 的fs.unlink删一个非空目录会报错,因为需要先递归清空内部文件。于是 rimraf 成了几乎所有开发工具链的底层依赖——很多前端脚手架和 CLI 工具的clean命令都依赖它。

我有一次给一个老项目升级依赖,发现package-lock.json里 rimraf 被十几个包间接依赖,版本还不统一。最尴尬的是我自己的代码里根本没用过它。

5.2 Node.js 原生的递归删除方案

Node.js 14.14.0 引入了fs.rmSync和fs.promises.rm,默认支持递归删除,recursive: true就对标了rm -rf。到 2026 年,这已经是一个非常稳定且性能良好的原生 API:

// 以前 const rimraf = require("rimraf"); rimraf.sync("./dist"); // 现在 const fs = require("fs"); fs.rmSync("./dist", { recursive: true, force: true }); // 异步版本 const fsPromises = require("fs/promises"); await fsPromises.rm("./dist", { recursive: true, force: true });

注意force: true的含义是“路径不存在时不报错”,对标rimraf的默认行为。没有force的话,删除一个不存在的路径会抛出错误。

5.3 替换后的实际收益

依赖层面,把项目里直接依赖的 rimraf 换成原生fs.rmSync后,node_modules的直接依赖数量会少一个,锁文件里的间接依赖也能少掉一大截。但我们实际清理时发现,问题出在那些把 rimraf 当成内部依赖的包上,比如许多旧的构建工具。

针对这种情况,不要尝试去替换间接依赖——那是别人的包,你控制不了。你应该做的是升级这些工具的主版本。新版本的工具基本都已经切换到原生fs.rm了。我在 2025 年年初给一个 Vite 项目做依赖升级,Vite 从 4.x 升到 6.x 后,锁文件里 rimraf 悄然消失了,没有任何手动干预。

经验:当你发现自己项目里某个包只是“间接依赖”时,不要手动去删node_modules里它的文件,也不要尝试改锁文件。正确做法是升级依赖树,让那些老工具包自然退出。

除了 rimraf,类似的案例还有del(另一个文件删除库)、fs-extra的remove()、mkdirp(用fs.mkdirSync(dir, { recursive: true })代替)——这些都可以用原生 API 替代。

6. 第五个可以删掉的包:dotenv 这样的环境变量加载器

6.1 dotenv 的职责

dotenv 的作用是:在 Node.js 应用启动时,把项目根目录的.env文件加载到process.env中。以前这是所有 Node.js 后端的标配,几乎所有 Express/Koa/NestJS 项目的启动入口都会有这么一行:

require("dotenv").config();

6.2 Node.js 原生支持已经到来

Node.js 20.6.0 加入了一个新 flag:--env-file。到 2024 年底,Node.js 22 已经把--env-file标记为稳定特性,支持加载.env文件到process.env,还支持.env.local这种多环境文件组合。

以 Node.js 22+ 为例,你只需要这样启动:

node --env-file=.env server.js

如果还想加载.env.local并让后者覆盖前者:

node --env-file=.env --env-file=.env.local server.js

在package.json的scripts里配置:

{ "scripts": { "dev": "node --env-file=.env server.js", "start": "node --env-file=.env --env-file=.env.local server.js" } }

到 2026 年,新的 LTS 版本 Node.js 对这一特性的支持已经不存在兼容性问题,测试和生产环境可以放心使用。

6.3 dotenv 还有哪些优势是原生不支持的

必须承认,dotenv 还是有一些原生方案没有覆盖的功能:

  • .env.example的自动生成和校验:dotenv 生态里有dotenv-parse-variables、dotenv-expand等配套工具,能让.env存储复杂类型、支持变量展开。
  • 运行时动态加载:如果你需要在应用启动之后、运行过程中加载某个指定的环境文件,而不仅仅是在启动命令里加参数,--env-file是做不到的。不过说实话,大多数应用在部署时都是一启动就确定环境,这个需求场景不多。
  • 旧版本 Node.js 兼容性:如果你还在维护一些跑在 Node 16 或 18 上的老服务,那确实只能继续用 dotenv。

所以在替换时不要想着一刀切。我的实操建议是:新项目直接用--env-file,老项目在逐步升级 Node 版本到 22+ 之后再迁移,不要在没有升级 Node 的情况下强行删 dotenv。

另外要提醒一个细节:--env-file模式下,如果.env文件不存在,Node.js 会直接报错退出(区别于 dotenv 默认静默失败)。这个行为差异在本地开发时特别容易踩,解决方法是确保配置文件存在,或者用.env.local这种总有默认值的文件。

7. 删包清扫行动中的实战笔记与避坑清单

7.1 动手之前先做依赖体检

我不建议拍脑袋决定删哪个包,也不建议看别人文章说“xx 可以删了就立刻删”。每个项目的情况不一样,别人的替代方案在你这边可能水土不服。我每次做依赖清理都会先跑一遍体检:

  1. 用npm ls查看某个包被哪些依赖引用,是直接依赖还是间接依赖。
npm ls rimraf npm ls axios npm ls dayjs
  1. 在项目里全局搜索这个包的 import 语句,确认实际用到的 API。
grep -r "from 'lodash'" src --include="*.js" --include="*.ts" grep -r "require('lodash')" src --include="*.js"
  1. 跑测试和构建,看看是否有遗漏。这一步最关键,code review 里看不出来的问题,跑一遍全量测试立刻现形。

建议在开始前先把package-lock.json或pnpm-lock.yaml备份一份,实在改坏了还能回滚。

7.2 两个最容易被忽视的坑

第一个坑是类型声明。像 axios、dayjs、lodash 这类库通常自带 TypeScript 类型定义。你用原生 API 替换后,如果项目里有一些类型在依赖这三个库的类型上,就可能编译报错。比如你把 axios 的AxiosResponse类型用在了业务接口定义里,替换成 fetch 后需要重新定义一套 API 类型。这类代码的迁移量往往比想象中大,建议先搜索所有相关类型引用再动手。

第二个坑是边界行为差异。lodash 的_.get会自动把"a[0].b"这样的字符串路径解析成数组下标,而原生可选链写起来更安全但不会自动解析字符串路径。如果你依赖这个特性,直接替换会踩坑。同样,axios 的transformResponse默认会尝试 JSON.parse,而 fetch 必须手动调用response.json()。这些边界差异需要你提前写单元测试来覆盖。

7.3 删包后的性能收益实测

有朋友问我,删掉这些包到底能快多少?我的实测数据供参考:

  • 一个中型前端项目(大概 300 个直接依赖),删掉 axios、dayjs、lodash、rimraf、dotenv 这五个包之后,npm install时间快了一倍左右,node_modules体积缩减了大约 25%。
  • 打包方面,如果用 webpack,bundle 体积理论上能缩小几十 KB 到几百 KB 不等,主要取决于你原本用了 lodash 的多少函数。Vite 的 rollup 打包也同理。
  • 运行时性能方面,这些库本身不算重,但原生 API 就运行在 V8 里,省去了解析和调用层,在大量日期格式化或者请求场景下,实测差距在 10% 到 30% 之间浮动。对大部分业务来说体感不明显,但确实更干净了。

更重要的收益其实是安全性和维护性:少一个间接依赖,就少一分供应链风险,少一分npm audit的噪音。这个收益没法量化,但长期看非常值。

7.4 什么时候不要删

最后一定要说清楚边界。虽然我建议大家在 2026 年删掉这五个包,但有一些例外情况,这时候不要强行动手:

  • 你要兼容的目标浏览器是 IE 或非常老的 WebView,那fetch、Object.groupBy、structuredClone这些全都不靠谱,axios 和 lodash 反而是安全的选择。
  • 项目里有大量历史代码深度依赖某个库的独特 API,比如用了 dayjs 的插件机制或者 axios 的拦截器链,迁移成本远大于收益时,留着也合理。
  • 你维护的是给外部开发者使用的公共 SDK,为了 API 兼容性和体验一致,保留这些库作为底层封装没有问题。

说到底,删包不是 KPI,不是删得越多越好。它是一个持续评估的过程:新写的代码尽量用原生 API,老代码在维护时顺手替换,不必为了赶时髦而大动干戈。

8. 一次真实的依赖清理实录

最后分享一个我实际操盘过的案例。2025 年年底,我接手了一个中型管理系统前端,Vue 3 + Vite 5 技术栈。拿到代码后第一件事是看package.json,好家伙,直接依赖 238 个,锁文件里 1700 多个包。CI 时间越来越长,本地安装一次依赖要三分钟。

我按上面的体检流程走了一遍:

  • axios:项目里其实只用了axios.get和axios.post,而且已经用fetch替代了一个模块,迁移顺利。
  • dayjs:主要用来格式化日志时间,配合Intl.DateTimeFormat半天改完,但有一个报表模块用了dayjs的timezone插件,需要额外处理时区,花了多一天。
  • lodash:用了_.get、_.cloneDeep、_.debounce、_.groupBy四个函数,全部替换成原生 API,顺便写了一个utils/debounce.ts公共函数。
  • rimraf:项目本身没有直接使用,但package.json的clean脚本用了rimraf dist,改成node -e "fs.rmSync('dist',{recursive:true,force:true})"后,顺手把这个依赖也移出去了。
  • dotenv:部署在 Node 20 上,--env-file可以直接用,把启动脚本改好之后,从 238 个直接依赖里减掉了 5 个。

实际迁移耗时:一个人,约四个工作日。中间还包括跑测试、修类型报错的时间。最后直接依赖从 238 降到 203(中间还顺带清理了其它过时包),npm install时间从三分钟降到一分半以内,CI 整体快了约两分钟。最让我高兴的是,npm audit报告的高危漏洞数从 17 个降到了 4 个——剩余 4 个都是间接依赖,跟我们的代码关系已经不大了。

这件事做下来我的最大感受是:很多依赖不是不能用,而是我们习惯了用而不去质疑它的必要性。每一次安装包的时候多问一句“一定要这个吗?原生能不能做?做起来要花多久?”——长期下来,项目的可维护性会有肉眼可见的提升。希望你也能从自己的项目里,找到那个等待被删掉的包。