1. 为什么 2026 年反而该给 node_modules 减肥了
先说明一下,我不是反对用第三方包。npm 生态是前端能够走到今天的关键,没有这两百多万个包,我们做不了这么多事。但这两年我越来越明显感受到一个趋势:浏览器和 Node.js 原生能力在快速补齐,很多当年“不得不装”的包,现在已经是纯粹的负担了 。 我说的负担,不是装完占几百 MB 硬盘这么简单。每个依赖都意味着安装时间变长、依赖树变复杂、安全审计面变大、供应链风险增加。你知道现在 npm 上最头疼的事情是什么吗?不是包不够用,而是你根本不知道你装的这个包的间接依赖里藏了什么。一个 扫出一堆漏洞,修都修不完,问题往往出在那些你根本没用到的间接依赖上。 我给团队定过一个规矩:新增一个依赖,必须写清楚它的不可替代性。如果一句话说不清楚,那就不许加。 别笑,实际执行下来大家发现,很多包其实早就有原生替代方案了,特别是下面这几个。 还有一个驱动因素是从 Node 14 到 Node 22 以及浏览器平台的持续迭代。原生 API 已经覆盖了我们日常开发中的绝大多数场景——日期处理、HTTP 请求、数组操作、文件删除、环境变量读取……而且原生 API 往往是 V8 直接实现的,性能和内存占用反而比引入一个包要好。JS 引擎的团队这些年在标准化上面花的力气是实打实的,我们这些使用者如果不跟进,就会被时代悄悄留在原地。 这篇文章里我挑出 5 个我实测可以在 2026 年从项目里删掉的包,每个都给出替代方案、迁移思路和踩坑记录。注意,我说的是“可以删掉”,不是“必须删掉”——如果你的代码库里已经用得很顺手,历史包袱也重,那不强求立刻动刀。但如果是在开新项目,或者正在做依赖治理,下面这 5 个包值得你认真看一遍。 几年前我们用 dayjs 或 moment.js,最核心的痛点是:原生 对象太不好用了。解析字符串要碰运气,格式化要靠手写拼凑,时区处理直接就是灾难。做国际化产品的时候,同一个时间在不同地区要显示成不同的文本,原生 API 完全帮不上忙。 moment.js 现在基本已经退场了,dayjs 成了最主流的替代品,体积小、API 友好、插件机制完善。我自己的很多老项目里也还在用 dayjs,说实话它确实好用,团队没人抱怨过。 但问题在于:我们用 dayjs 解决的三个核心问题——格式化、相对时间、时区处理——原生 API 在 2026 年全都已经有成熟方案了。 先说最基础的格式化。 参数已经能覆盖绝大多数业务场景: 再说相对时间。以前要显示“3 天前”“2 小时前”,需要自己写逻辑或者借助 dayjs 的 relativeTime 插件。现在 原生支持: 最后是时区。这个是 dayjs 的 timezone 插件最核心的功能,原生方案是 不知道高到哪里去了。 注意:Temporal 到 2026 年仍然建议只在实验性项目里使用,生产环境优先考虑 系列 API,它们已经足够成熟稳定。 我的建议是分三步走: 踩过的坑主要有两个: 这不算复杂,但确实需要你花半天到一天时间去适配。 axios 可以说是前端最知名的网络请求库,它的核心优势在于请求/响应拦截器、取消请求、超时控制、自动 JSON 转换和浏览器环境适配。在 fetch 还没普及的年代, 的 API 设计太反人类,axios 把那一层封装得足够好用,所以它成为了事实标准。 但现在我们得认真看一下:原生 已经是所有现代浏览器和 Node.js 全局支持的标准 API。2024 年和 2025 年,Node.js 的 fetch 实现(基于 undici)已经非常稳定,之前所谓“Node 18 的 fetch 还不靠谱”“上传文件有 bug”这些问题基本都解决了。到 2026 年,我至少在自己维护的十几个项目里都用纯 fetch 跑过生产流量,没有发现任何一例由于 fetch 本身导致的稳定性问题。 先用一张表来对比,这样比较直观: 核心要点是拦截器。axios 的拦截器确实是好用,但说白了就是一个洋葱模型,用原生 fetch 也能很容易地实现同样效果: 我在实际迁移中也发现一个场景,fetch 目前确实不如 axios 方便:上传文件并监听进度。fetch 的原生 的上传工具函数,其它场景全部换成 fetch。 另外一个场景是超时和重试的组合逻辑。axios 上有 这样的库,原生 fetch 没有直接等价物。但这个问题也不难解决,自己写一个约五十行的重试工具就好,并不复杂。 lodash 是 npm 下载量最高的包之一,但如果你仔细审视自己的代码,会发现你真正用到的函数可能不超过十个: ……这些函数确实帮我们省了不少事。 但问题在于,从 ES6 开始,JavaScript 原生就一直在补齐数组、对象、字符串的处理能力。到了 ES2023,新增了 等等。到 2026 年回头看,lodash 最核心的那十几个工具函数,原生 API 已经能覆盖掉至少八成以上。 撬开核心的几个看: 这里重点说一下 ,实测下来没有问题。 另一个容易被忽略的是 就好,原生支持不需要任何填充。 说实话,lodash 有一些函数原生至今没有等价物: 我的建议是:不要一次性把 lodash 全删掉。先把不需要的部分拆出去,用原生替代。最后如果只剩少数几个函数,直接复制它们的功能到项目里(代码量很小),或者用 lodash 的按函数引入( 这种路径)方式,而不是全部引入。 rimraf 这个包有多经典?它是对 命令都依赖它。 我有一次给一个老项目升级依赖,发现 里 rimraf 被十几个包间接依赖,版本还不统一。最尴尬的是我自己的代码里根本没用过它。 Node.js 14.14.0 引入了 。到 2026 年,这已经是一个非常稳定且性能良好的原生 API: 注意 的话,删除一个不存在的路径会抛出错误。 依赖层面,把项目里直接依赖的 rimraf 换成原生 的直接依赖数量会少一个,锁文件里的间接依赖也能少掉一大截。但我们实际清理时发现,问题出在那些把 rimraf 当成内部依赖的包上,比如许多旧的构建工具。 针对这种情况,不要尝试去替换间接依赖——那是别人的包,你控制不了。你应该做的是升级这些工具的主版本。新版本的工具基本都已经切换到原生 了。我在 2025 年年初给一个 Vite 项目做依赖升级,Vite 从 4.x 升到 6.x 后,锁文件里 rimraf 悄然消失了,没有任何手动干预。 经验:当你发现自己项目里某个包只是“间接依赖”时,不要手动去删 里它的文件,也不要尝试改锁文件。正确做法是升级依赖树,让那些老工具包自然退出。 除了 rimraf,类似的案例还有 代替)——这些都可以用原生 API 替代。 dotenv 的作用是:在 Node.js 应用启动时,把项目根目录的 中。以前这是所有 Node.js 后端的标配,几乎所有 Express/Koa/NestJS 项目的启动入口都会有这么一行: Node.js 20.6.0 加入了一个新 flag: 这种多环境文件组合。 以 Node.js 22+ 为例,你只需要这样启动: 如果还想加载 并让后者覆盖前者: 在 里配置: 到 2026 年,新的 LTS 版本 Node.js 对这一特性的支持已经不存在兼容性问题,测试和生产环境可以放心使用。 必须承认,dotenv 还是有一些原生方案没有覆盖的功能: 所以在替换时不要想着一刀切。我的实操建议是:新项目直接用 ,老项目在逐步升级 Node 版本到 22+ 之后再迁移,不要在没有升级 Node 的情况下强行删 dotenv。 另外要提醒一个细节: 这种总有默认值的文件。 我不建议拍脑袋决定删哪个包,也不建议看别人文章说“xx 可以删了就立刻删”。每个项目的情况不一样,别人的替代方案在你这边可能水土不服。我每次做依赖清理都会先跑一遍体检: 建议在开始前先把 备份一份,实在改坏了还能回滚。 第一个坑是类型声明。像 axios、dayjs、lodash 这类库通常自带 TypeScript 类型定义。你用原生 API 替换后,如果项目里有一些类型在依赖这三个库的类型上,就可能编译报错。比如你把 axios 的 类型用在了业务接口定义里,替换成 fetch 后需要重新定义一套 API 类型。这类代码的迁移量往往比想象中大,建议先搜索所有相关类型引用再动手。 第二个坑是边界行为差异。lodash 的 。这些边界差异需要你提前写单元测试来覆盖。 有朋友问我,删掉这些包到底能快多少?我的实测数据供参考: 更重要的收益其实是安全性和维护性:少一个间接依赖,就少一分供应链风险,少一分 的噪音。这个收益没法量化,但长期看非常值。 最后一定要说清楚边界。虽然我建议大家在 2026 年删掉这五个包,但有一些例外情况,这时候不要强行动手: 说到底,删包不是 KPI,不是删得越多越好。它是一个持续评估的过程:新写的代码尽量用原生 API,老代码在维护时顺手替换,不必为了赶时髦而大动干戈。 最后分享一个我实际操盘过的案例。2025 年年底,我接手了一个中型管理系统前端,Vue 3 + Vite 5 技术栈。拿到代码后第一件事是看 ,好家伙,直接依赖 238 个,锁文件里 1700 多个包。CI 时间越来越长,本地安装一次依赖要三分钟。 我按上面的体检流程走了一遍: 实际迁移耗时:一个人,约四个工作日。中间还包括跑测试、修类型报错的时间。最后直接依赖从 238 降到 203(中间还顺带清理了其它过时包), 报告的高危漏洞数从 17 个降到了 4 个——剩余 4 个都是间接依赖,跟我们的代码关系已经不大了。 这件事做下来我的最大感受是:很多依赖不是不能用,而是我们习惯了用而不去质疑它的必要性。 每一次安装包的时候多问一句“一定要这个吗?原生能不能做?做起来要花多久?”——长期下来,项目的可维护性会有肉眼可见的提升。希望你也能从自己的项目里,找到那个等待被删掉的包。npm install下去,锁文件里多出四五百个包是常态,里面可能有一半你听都没听说过。每次npm audit2. 第一个可以删掉的包:dayjs 这样的日期处理库
2.1 dayjs 曾经解决什么问题
Date2.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"Intl.RelativeTimeFormatconst rtf = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto" }); rtf.format(-3, "day"); // "3天前" rtf.format(2, "hour"); // "2小时后"Intl.DateTimeFormat的timeZone参数,以及Date.prototype.toLocaleString的timeZone选项。2024 年之后,TemporalAPI 的提案逐步落地,虽然到 2026 年还没有在所有运行时默认开启,但已经可以在多数主流浏览器和 Node.js 22+ 的 flag 下使用了。Temporal 把日期、时间、时区、持续时间这些概念完全分开,设计上比 JavaScript 的DateIntl2.3 迁移方案和坑
dayjs().format(...)调用列出来,逐个用Intl.DateTimeFormat替换。多数情况下只需封装一个formatDate(date, pattern)工具函数。Intl.RelativeTimeFormat。这个 API 的浏览器兼容性在 2026 年已经非常好,Node.js 侧也完全没有压力。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 的历史地位和现状
XMLHttpRequestfetch3.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或流式上传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
Request和Response对象都是基于 stream 设计的,理论上可以监听底层流的进度,但实现起来非常繁琐,而且并不标准。不过在实际业务里,文件上传场景我们已经有更好的选择——XMLHttpRequest的upload.onprogress是最成熟可靠的方案,或者直接使用二进制流加一个ReadableStream包一层。我自己一般会保留一个基于XMLHttpRequestaxios-retry4. 第三个可以删掉的包:lodash 这样的工具函数库
4.1 lodash 的定位和实际使用率
_.debounce、_.throttle、_.cloneDeep、_.groupBy、_.isEqual、_.get、_.setArray.prototype.toSorted、toReversed、with、findLast等一批数组方法;ES2024 又增加了Object.groupBy和Map.groupBy;再加上已有的Object.fromEntries、Object.hasOwn、Array.flatMap、Array.at4.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替换掉了所有_.cloneDeepObject.hasOwn。以前判断一个对象是否有某个自有属性,要用Object.prototype.hasOwnProperty.call(obj, key),特别啰嗦。lodash 的_.has也有类似的坑。现在直接用Object.hasOwn(obj, key)4.3 lodash 哪些部分仍然值得保留
_.merge:递归合并多个对象,且能正确处理数组、嵌套对象。原生Object.assign和展开语法都是浅拷贝,实现深度合并仍然需要自己写或者借助其它小工具包。_.pick/_.omit:选取或排除对象属性的工具函数,原生没有直接等价物,但用解构加 rest 语法也能实现大部分场景。_.uniqBy/_.sortedUniq:在某些特定数据结构上确实好用。lodash/merge5. 第四个可以删掉的包:rimraf 这样的删除文件工具
5.1 rimraf 的历史背景
rm -rf的跨平台实现。在 Windows 和 macOS 上,直接用 Node.js 的fs.unlink删一个非空目录会报错,因为需要先递归清空内部文件。于是 rimraf 成了几乎所有开发工具链的底层依赖——很多前端脚手架和 CLI 工具的cleanpackage-lock.json5.2 Node.js 原生的递归删除方案
fs.rmSync和fs.promises.rm,默认支持递归删除,recursive: true就对标了rm -rf// 以前 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的默认行为。没有force5.3 替换后的实际收益
fs.rmSync后,node_modulesfs.rmnode_modulesdel(另一个文件删除库)、fs-extra的remove()、mkdirp(用fs.mkdirSync(dir, { recursive: true })6. 第五个可以删掉的包:dotenv 这样的环境变量加载器
6.1 dotenv 的职责
.env文件加载到process.envrequire("dotenv").config();6.2 Node.js 原生支持已经到来
--env-file。到 2024 年底,Node.js 22 已经把--env-file标记为稳定特性,支持加载.env文件到process.env,还支持.env.localnode --env-file=.env server.js.env.localnode --env-file=.env --env-file=.env.local server.jspackage.json的scripts{ "scripts": { "dev": "node --env-file=.env server.js", "start": "node --env-file=.env --env-file=.env.local server.js" } }6.3 dotenv 还有哪些优势是原生不支持的
.env.example的自动生成和校验:dotenv 生态里有dotenv-parse-variables、dotenv-expand等配套工具,能让.env存储复杂类型、支持变量展开。--env-file是做不到的。不过说实话,大多数应用在部署时都是一启动就确定环境,这个需求场景不多。--env-file--env-file模式下,如果.env文件不存在,Node.js 会直接报错退出(区别于 dotenv 默认静默失败)。这个行为差异在本地开发时特别容易踩,解决方法是确保配置文件存在,或者用.env.local7. 删包清扫行动中的实战笔记与避坑清单
7.1 动手之前先做依赖体检
npm ls查看某个包被哪些依赖引用,是直接依赖还是间接依赖。npm ls rimraf npm ls axios npm ls dayjsgrep -r "from 'lodash'" src --include="*.js" --include="*.ts" grep -r "require('lodash')" src --include="*.js"package-lock.json或pnpm-lock.yaml7.2 两个最容易被忽视的坑
AxiosResponse_.get会自动把"a[0].b"这样的字符串路径解析成数组下标,而原生可选链写起来更安全但不会自动解析字符串路径。如果你依赖这个特性,直接替换会踩坑。同样,axios 的transformResponse默认会尝试 JSON.parse,而 fetch 必须手动调用response.json()7.3 删包后的性能收益实测
npm install时间快了一倍左右,node_modules体积缩减了大约 25%。npm audit7.4 什么时候不要删
fetch、Object.groupBy、structuredClone这些全都不靠谱,axios 和 lodash 反而是安全的选择。8. 一次真实的依赖清理实录
package.jsonaxios:项目里其实只用了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 个。npm install时间从三分钟降到一分半以内,CI 整体快了约两分钟。最让我高兴的是,npm audit