news 2026/9/18 12:09:37

2026年可删除的npm包:原生能力替代与依赖清理实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2026年可删除的npm包:原生能力替代与依赖清理实践

1. 为什么“删包”这件事值得认真对待

前端生态有个很拧巴的现象:我们一边抱怨node_modules体积失控、安装慢、CI 卡在npm install上,一边又在package.json里堆着一堆“当年为了兼容某个环境装上去、后来再也没动过”的依赖。这些包平时不声不响,直到某天你升级 Node 版本、跑一次npm audit、或者换了个构建工具,它们才跳出来刷存在感——要么报废弃警告,要么直接编译失败。

我最近花了两天时间,把一个维护了三年多的中后台项目从 60 多个直接依赖砍到 40 出头,node_modules从 480MB 降到 310MB,CI 安装时间从 3 分 20 秒压到 1 分 50 秒。整个过程没有改一行业务逻辑,纯粹是“删掉那些已经不需要的包”。这件事让我意识到:依赖清理不是洁癖,而是一次低风险、高回报的技术债偿还

这篇文章要聊的,就是 2026 年这个时间点上,哪些 npm 包已经可以被原生能力、语言标准或者更轻的方案替代,可以放心从package.json里删掉。我会把每个包的“为什么当年要装”“现在为什么能删”“删之前怎么验证”“删之后有什么坑”讲清楚,而不是甩一句“用原生 API 就行”。适合所有还在维护 JavaScript/TypeScript 项目的人——不管你是刚接手老项目的新人,还是想给项目减负的老手。

需要先说明一点:下面提到的“可以删”,前提是你的项目运行环境满足对应条件(比如 Node 版本、浏览器目标、构建工具链)。如果你的项目还要兼容很老的运行时,那有些包该留还得留。我会在每个包里标注清楚适用边界。

2. 被原生能力吃掉的工具包:从 lodash 到 uuid

2.1 lodash:从“必备”到“按需”

lodash 曾经是前端项目的标配,_.get_.debounce_.cloneDeep几乎是肌肉记忆。但现在情况变了。

为什么当年要装:ES5 时代,Array.prototype.findObject.assign、可选链这些都不存在,lodash 提供了一套跨浏览器的统一工具集,还顺手解决了深拷贝、防抖节流这些原生没有的能力。

现在为什么能删:分两块看。

第一块是语言标准补齐的。可选链?.、空值合并??Array.prototype.flatObject.entriesObject.fromEntriesString.prototype.replaceAll这些在 ES2020 之后全部进入标准,现代浏览器和 Node 14+ 都原生支持。_.get(obj, 'a.b.c')直接写成obj?.a?.b?.c,语义更清晰,还没有函数调用开销。

第二块是原生 API 覆盖的structuredClone在 Node 17+ 和现代浏览器里已经可用,深拷贝不再必须_.cloneDeep。防抖节流虽然原生没有,但自己写一个 20 行的实现比引入整个 lodash 划算得多。

// 自己实现的 debounce,够用且可控 function debounce(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }

怎么删:不要直接npm uninstall lodash就完事。先用grep -rn "lodash" src/或者rg "from 'lodash'"把所有引用点找出来,逐个替换。如果项目里用了lodash/fp或者lodash-es,替换成本会高一些,因为函数式风格的链式调用原生没有直接对应。

踩坑提醒_.get和可选链有一个行为差异——_.get对数组下标也生效,arr?.[0]写法上稍微啰嗦一点。另外_.isEqual的深比较逻辑比JSON.stringify对比靠谱得多,如果项目里用它做对象比较,替换时要小心,建议保留一个精简的isEqual实现或者换成fast-deep-equal这种 1KB 级别的小包。

2.2 uuid:crypto.randomUUID 已经够用

uuid包在生成唯一标识的场景里出场率极高。但 Node 14.17+ 和现代浏览器都提供了crypto.randomUUID(),直接返回标准 UUID v4 字符串。

// 以前 import { v4 as uuidv4 } from 'uuid'; const id = uuidv4(); // 现在 const id = crypto.randomUUID();

边界条件crypto.randomUUID()只在安全上下文(HTTPS 或 localhost)下可用。如果你的页面可能跑在 HTTP 环境下,那这个 API 会返回 undefined。这种情况下要么保留 uuid 包,要么用crypto.getRandomValues自己拼一个。

性能对比:实测下来,crypto.randomUUID()比 uuid 包快大约 3 到 5 倍,因为它直接调用底层实现,没有 JS 层的字符串拼接和校验逻辑。对于高频生成 ID 的场景(比如前端批量创建临时对象),这个差距是能感知到的。

2.3 node-fetch:Node 18 之后没必要了

node-fetch是 Node 环境里用 Fetch API 的经典方案。但 Node 18 开始,全局fetch已经内置,而且是稳定可用的。

为什么当年要装:Node 早期没有 fetch,只有http/https模块,写起来又臭又长。node-fetch 把浏览器那套 Fetch API 搬到了 Node,统一了前后端写法。

现在为什么能删:Node 18+ 的全局 fetch 基于 undici 实现,性能比 node-fetch 更好,而且支持AbortControllerReadableStream这些现代特性。node-fetch 本身在 3.x 之后也基本进入维护模式,官方 README 里都建议新项目直接用原生 fetch。

迁移注意:node-fetch 和原生 fetch 在几个细节上有差异。一是response.body的类型,node-fetch 返回 Node Stream,原生 fetch 返回 Web Stream,如果你用.pipe()处理响应体,需要改成Readable.fromWeb()。二是错误处理,原生 fetch 只在网络错误时 reject,HTTP 4xx/5xx 不会 reject,需要手动检查response.ok

// 迁移前 const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); // 迁移后逻辑一样,但要注意 body 类型变化 const data = await res.json();

2.4 小结:判断一个工具包是否该删的通用方法

不是所有工具包都能一刀切删掉。我总结了一个判断流程:

判断维度可以删的信号需要保留的信号
语言标准功能已进入 ES 标准且目标环境支持依赖提案阶段特性
运行时内置Node/浏览器已原生提供需要兼容旧运行时
使用频率全项目只用了一两个函数深度依赖其链式/插件体系
替代成本替换后代码量不增反减替换需要大量重写和测试
维护状态官方已标记 deprecated仍在活跃维护且有独特能力

按这个表过一遍,你会发现项目里至少有三五个包是可以直接删的。

3. 构建与转译环节里那些“历史遗留”

3.1 babel 相关插件:当目标环境已经支持新语法

很多项目的devDependencies里躺着一长串@babel/plugin-*,比如@babel/plugin-proposal-optional-chaining@babel/plugin-proposal-nullish-coalescing-operator。这些插件当年是为了让新语法能在旧环境跑起来。

现在的情况:可选链和空值合并早在 ES2020 就正式标准化了,@babel/preset-env会根据你的browserslist自动决定要不要转译。也就是说,你根本不需要单独装这些 proposal 插件,preset-env 已经把它们包含进去了。

怎么清理:打开babel.config.js.babelrc,把所有@babel/plugin-proposal-*里对应已经进入 Stage 4 的语法插件删掉,只保留 preset-env。然后跑一遍构建,看有没有报错。如果browserslist配置合理(比如> 0.5%, last 2 versions, not dead),preset-env 会自动处理。

踩坑提醒:有些项目用的是@babel/plugin-transform-*而不是proposal-*,这些是已经标准化的语法转换插件。如果你手动列了这些插件,其实也是多余的,preset-env 会按需引入。删掉它们不会影响构建结果,但能减少 Babel 配置的复杂度。

3.2 core-js:按需引入比全量引入更值得

core-js是 polyfill 领域的老大,但它的全量引入方式(import 'core-js')会把所有 polyfill 都打进产物,体积轻松超过 200KB。

为什么当年要装:为了兼容 IE 和早期浏览器,需要补上 Promise、Array.from、Object.assign 这些 API。

现在怎么处理:如果你的browserslist已经排除了 IE 和很老的浏览器,core-js 的很多模块根本不会被触发。更好的做法是用core-js/stable配合useBuiltIns: 'usage',让 Babel 根据实际代码里用到的 API 自动按需引入。

// babel.config.js module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, }], ], };

这样配置之后,你不需要手动import 'core-js',Babel 会在用到PromiseArray.prototype.includes这些 API 时自动插入对应的 polyfill 引入。产物体积能降不少。

什么时候该彻底删掉 core-js:如果你的项目只跑在 Node 18+ 或者只面向现代浏览器(Chrome 90+、Firefox 88+、Safari 14+),那 core-js 基本可以整个删掉。判断方法很简单:把 core-js 从依赖里移除,跑一遍完整测试,如果没报错,说明目标环境已经原生支持你用到的所有 API。

3.3 autoprefixer:当 browserslist 已经足够新

autoprefixer是 PostCSS 生态里给 CSS 加厂商前缀的插件。但现代浏览器对 flexbox、grid、transition 这些属性的支持已经非常统一,很多前缀已经不需要了。

判断方法:看你的browserslist配置。如果里面还有ie 11android 4.4这种,那 autoprefixer 还得留着。如果已经是last 2 versionsnot dead,那 autoprefixer 加的前缀会非常少,甚至为零。

实测数据:我在一个browserslist: ['> 0.5%', 'last 2 versions', 'not dead']的项目里,把 autoprefixer 移除后对比 CSS 产物,gzip 后只差了 1.2KB。为了这 1.2KB 保留一个构建插件和它的依赖树,不太划算。

但要注意:autoprefixer 不只是加前缀,它还会处理一些语法降级,比如gap在旧 flexbox 里的兼容写法。删之前建议用npx autoprefixer --info看看当前配置下实际会加哪些前缀,如果输出为空或者只有一两条,就可以放心删。

3.4 一个真实的清理案例

我拿一个 Vue 2 项目做过实验,devDependencies里有@babel/plugin-proposal-optional-chaining@babel/plugin-proposal-nullish-coalescing-operator@babel/plugin-proposal-class-properties三个插件。删掉之后,构建产物大小没变(因为 preset-env 本来就会处理),但 Babel 配置从 40 行降到 15 行,构建速度提升了约 8%。

这个提升不算大,但配置简化带来的维护收益是长期的——下次升级 Babel 版本时,少三个插件要跟着升,少三个可能出兼容问题的地方。

4. 那些被更轻方案替代的“大块头”

4.1 moment.js:dayjs 或原生 Intl 都能接

moment.js是日期处理领域曾经的绝对王者,但它的体积(压缩后约 70KB,gzip 后约 20KB)和不可 tree-shaking 的设计让它成了性能优化的重点对象。

为什么当年要装:原生DateAPI 难用,格式化、解析、时区计算都要自己写。moment 提供了一套直观的链式 API。

现在为什么能删:两个方向。

方向一是换dayjs。dayjs 的 API 和 moment 高度兼容,体积只有 2KB,而且支持按需加载插件。迁移成本很低,大部分代码只需要把import moment from 'moment'改成import dayjs from 'dayjs',然后把moment()改成dayjs()

方向二是用原生Intl。如果你只需要格式化和简单的日期计算,Intl.DateTimeFormatIntl.RelativeTimeFormat已经能覆盖大部分场景。

// 格式化 new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()); // 相对时间 new Intl.RelativeTimeFormat('zh-CN').format(-1, 'day'); // "1天前"

踩坑提醒:moment 的moment().add(1, 'month')在月末有特殊行为(比如 1 月 31 日加一个月会变成 2 月 28 日),dayjs 默认行为一致,但原生 Date 需要自己处理。如果项目里有大量日期计算逻辑,建议还是用 dayjs,不要硬上原生。

4.2 axios:fetch 封装一下就够了

axios在很长一段时间里是 HTTP 请求的首选,因为它解决了 fetch 的几个痛点:自动 JSON 转换、请求/响应拦截器、超时设置、错误处理。

现在的情况:fetch 本身还是那样,但围绕 fetch 的轻量封装已经成熟。如果你不需要 axios 的拦截器体系,自己写一个 50 行的封装就能覆盖 90% 的场景。

async function request(url, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), options.timeout || 10000); try { const res = await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } finally { clearTimeout(timeout); } }

什么时候该保留 axios:如果你的项目重度依赖拦截器做统一鉴权、错误提示、请求重试,那 axios 的拦截器体系确实省事。但即便如此,也可以考虑换成kywretch这类基于 fetch 的轻量库,体积只有 axios 的三分之一左右。

实测对比:在一个中型项目里,把 axios 换成自封装 fetch 后,打包体积减少约 12KB(gzip),代码里少了 3 个拦截器配置文件,但需要自己处理超时和错误重试。整体来说,如果团队对 fetch 熟悉,这个替换是值得的。

4.3 classnames:模板字符串不香吗

classnames是一个很小的包(约 1KB),用来拼接 CSS 类名。它的 API 很简洁:

classNames('foo', { bar: true, baz: false }); // "foo bar"

为什么能删:模板字符串加三元表达式完全能替代,而且更直观:

`foo ${bar ? 'bar' : ''} ${baz ? 'baz' : ''}`.trim()

如果觉得这样写太啰嗦,可以自己封装一个 10 行的工具函数,没必要引入一个包。

但要注意:classnames 在处理数组、嵌套对象、多参数混合时确实方便,如果项目里大量使用它的复杂用法,替换成本可能高于收益。我的建议是:新项目直接用模板字符串,老项目如果 classnames 用得很深,可以保留,毕竟它只有 1KB。

4.4 替换决策的通用框架

面对一个“可能可以删”的包,我一般按这个流程走:

  1. 查使用量rg "from 'package-name'"统计引用点数量。如果只有一两个文件用,替换成本极低。
  2. 查替代方案:原生 API 是否覆盖?有没有更轻的同类包?替代方案的 API 差异有多大?
  3. 查维护状态npm view package-name time.modified看最后更新时间。如果超过两年没更新,且社区有活跃替代品,优先考虑替换。
  4. 小范围验证:先在一个分支上替换,跑完整测试,对比构建产物大小和构建时间。
  5. 灰度发布:如果项目有灰度能力,先在小流量环境验证,确认无异常后再全量。

这个流程走下来,大部分替换决策都能在半天内完成验证。

5. 删包之前必须做的验证与回滚准备

5.1 依赖分析:先搞清楚谁在用、谁被用

删包最怕的不是删错,而是删了之后某个间接依赖挂了。比如你删了lodash,但某个第三方组件内部依赖lodash.get,那构建时可能不报错,运行时才炸。

工具推荐npm ls <package-name>可以看某个包的依赖树,npx depcheck可以扫描项目里声明了但没用的依赖,npx knip更激进,能找出未使用的文件和导出。

# 查看 lodash 被谁依赖 npm ls lodash # 扫描未使用的依赖 npx depcheck # 更全面的未使用代码/依赖扫描 npx knip

我的习惯:删包之前先跑一次npm ls <package>,确认没有其他包依赖它。如果有,要么保留,要么连同上层包一起替换。

5.2 构建产物对比:用数据说话

删包之后,构建产物大小和构建时间是两个最直观的指标。我一般会在删之前先跑一次构建,记录 baseline:

# 记录构建时间和产物大小 time npm run build du -sh dist/

删完之后再跑一次,对比数据。如果产物大小没降反升,说明替换方案引入了更大的依赖,需要重新评估。

注意:产物大小要看 gzip 后的数值,因为 HTTP 传输是压缩的。du -sh看的是原始大小,可以用gzip -r dist/或者构建工具自带的体积分析插件来看压缩后大小。

5.3 测试覆盖:没有测试就别乱删

这是最重要的一条:如果项目没有足够的测试覆盖,不要大规模删包

删包本质上是一次重构,重构没有测试兜底就是在赌博。我见过太多“删了一个以为没用的包,结果线上某个边缘功能挂了”的案例。

最低要求:核心业务流程有端到端测试,工具函数有单元测试。如果测试覆盖率很低,建议先补测试,再考虑删包。或者采用更保守的策略:一次只删一个包,删完手动回归一遍核心功能。

5.4 回滚方案:删包也要有 Plan B

删包之前,确保你能快速回滚。最简单的方式是在独立分支上操作,确认无误后再合并。如果项目使用 monorepo 或者有发布流程,可以先发一个 beta 版本,观察一段时间再正式发布。

# 创建清理分支 git checkout -b chore/remove-unused-deps # 删包并提交 npm uninstall lodash moment axios git add package.json package-lock.json git commit -m "chore: remove lodash, moment, axios" # 验证通过后合并 git checkout main git merge chore/remove-unused-deps

锁文件一定要提交package-lock.jsonpnpm-lock.yaml必须跟着package.json一起提交,否则 CI 环境可能装出不同的依赖树,导致“本地能跑、CI 报错”的经典问题。

5.5 一个真实的翻车案例

我有一次在一个 React 项目里删掉了core-js,因为browserslist已经排除了 IE。本地构建和测试都通过了,但上线后收到反馈:部分安卓低端机白屏。

排查后发现,那些设备的内置浏览器内核版本虽然不算太老,但对Promise.prototype.finally的支持有缺陷,而 core-js 之前一直在默默补这个洞。browserslistlast 2 versions并没有覆盖到这些定制内核。

教训browserslist是基于主流浏览器版本判断的,但实际用户环境可能更复杂。删 polyfill 类包时,要么保留最小化的按需引入,要么确保有真实设备或云真机测试覆盖。

6. 删完之后:如何防止依赖再次膨胀

6.1 建立依赖准入规则

删包只是一次性动作,如果不建立规则,过半年package.json又会膨胀回去。我一般会跟团队约定几条规则:

  • 新增依赖前先问:原生 API 能不能做?项目里有没有现成的工具函数?
  • 如果必须新增,优先选体积小于 5KB、周下载量大于 100 万、最近半年有更新的包。
  • 禁止引入功能重复的包(比如已经用了 dayjs,就不准再装 date-fns)。

这些规则不需要写成正式文档,在 code review 时口头把关就行。关键是让团队形成“引入依赖是有成本的”这个意识。

6.2 用工具做持续监控

人工把关容易漏,可以借助工具做自动化检查:

  • npm auditpnpm audit检查安全漏洞。
  • npx depcheck定期扫描未使用依赖,可以加到 CI 里作为非阻塞检查。
  • bundlesizesize-limit监控构建产物大小,超过阈值时报警。
  • npm-check交互式检查过时和未使用的依赖。
// package.json 里加 size-limit 配置 { "size-limit": [ { "path": "dist/index.js", "limit": "150 KB" } ] }

这样每次 PR 都会检查产物大小,一旦超标就会提醒。时间长了,团队会自觉控制依赖引入。

6.3 定期做依赖体检

我习惯每季度花半天时间做一次依赖体检:

  1. npx depcheck找出未使用的依赖。
  2. npm outdated看过时依赖,评估升级成本。
  3. 检查package.json里有没有可以用原生替代的包。
  4. 更新browserslist配置,移除已经不需要兼容的旧版本。

这个习惯坚持两年下来,项目的依赖数量一直控制在合理范围内,没有出现过“依赖地狱”。

6.4 关于“删包”的心态

最后说点个人体会。删包这件事,最大的阻力往往不是技术,而是心理——总觉得“万一以后要用呢”“删了会不会出问题”。这种心态可以理解,但代价是项目越来越重,构建越来越慢,新人上手越来越难。

我的建议是:把依赖当成负债而不是资产。每多一个依赖,就多一份升级负担、多一个安全风险点、多一层构建复杂度。能删就删,删不了就换成更轻的,换不了就至少搞清楚它为什么还在。

2026 年的 JavaScript 生态已经比五年前成熟太多,很多当年必须依赖第三方包的能力,现在原生就能做,而且做得更好。花一个下午清理一下package.json,你会发现项目轻了,构建快了,心里也踏实了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 12:09:21

C盘爆满怎么办?用Windows自带功能安全清理,释放几十GB

C盘快满了不敢乱删&#xff1f;这份清理指南教你安全腾出几十GB遇到C盘爆红这种事儿&#xff0c;我太理解了。系统盘空间告急的时候&#xff0c;Windows会变得卡顿、软件动不动报错、更新也装不上&#xff0c;最难受的是你打开资源管理器一瞅&#xff0c;明明没装几个大软件&am…

作者头像 李华
网站建设 2026/9/18 12:08:12

具身智能仿真平台Habitat安装避坑:从零跑通example.py

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 12:07:08

RuoYi 从 MySQL 迁移 PostgreSQL:SQL 适配与避坑实战

1. 迁移前先想清楚&#xff1a;为什么要动数据库把 Ruoyi 从 MySQL 迁到 PostgreSQL&#xff0c;这件事本身不难&#xff0c;难的是迁完之后系统还能原样跑起来。我前前后后在三套 Ruoyi 项目上做过这种切换&#xff0c;有 RuoYi-Vue 单体版的&#xff0c;也有 RuoYi-Cloud 拆成…

作者头像 李华
网站建设 2026/9/18 12:04:27

MySQL在Windows上的完整安装配置指南:从下载到排错

说实话&#xff0c;我见过太多人在MySQL上栽跟头了。有人从网上随便找了个安装包&#xff0c;一路Next装完&#xff0c;结果打开命令行一闪而过&#xff1b;有人好不容易装好了&#xff0c;写代码连库却报Access denied&#xff1b;还有人把数据库折腾了一整天&#xff0c;最后…

作者头像 李华
网站建设 2026/9/18 12:04:16

防窥膜行业研究报告自动化:Python数据流水线与PPTX生成

简介&#xff1a;这份防窥膜行业研究PPT面向市场分析人员、企业战略与投资决策者&#xff0c;以及关注消费电子功能膜赛道的从业者&#xff0c;可用于快速了解行业格局、梳理竞争要素并辅助项目论证。内容围绕防窥膜的定义与工作原理展开&#xff0c;依次覆盖中国防窥膜行业发展…

作者头像 李华