
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 迁移方案和坑我的建议是分三步走把项目里所有dayjs().format(...)调用列出来逐个用Intl.DateTimeFormat替换。多数情况下只需封装一个formatDate(date, pattern)工具函数。把相对时间逻辑替换为Intl.RelativeTimeFormat。这个 API 的浏览器兼容性在 2026 年已经非常好Node.js 侧也完全没有压力。最后把 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 可以删了就立刻删”。每个项目的情况不一样别人的替代方案在你这边可能水土不服。我每次做依赖清理都会先跑一遍体检用npm ls查看某个包被哪些依赖引用是直接依赖还是间接依赖。npm ls rimraf npm ls axios npm ls dayjs在项目里全局搜索这个包的 import 语句确认实际用到的 API。grep -r from lodash src --include*.js --include*.ts grep -r require(lodash) src --include*.js跑测试和构建看看是否有遗漏。这一步最关键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%。打包方面如果用 webpackbundle 体积理论上能缩小几十 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 个都是间接依赖跟我们的代码关系已经不大了。这件事做下来我的最大感受是很多依赖不是不能用而是我们习惯了用而不去质疑它的必要性。每一次安装包的时候多问一句“一定要这个吗原生能不能做做起来要花多久”——长期下来项目的可维护性会有肉眼可见的提升。希望你也能从自己的项目里找到那个等待被删掉的包。