前几天我跑了一下
npx depcheck,发现项目里有 47 个依赖,其中至少 6 个完全可以用浏览器原生 API 替代。卸载之后,打包体积直接少了 82KB(gzip 后少了 23KB),首屏加载快了 300ms。这篇文章把每个包的原生替代方案都写出来,附迁移代码,直接抄。
为什么要清理依赖
每多一个 npm 包,你的项目就多了:
- 打包体积:用户每次访问都要多下载这些代码
- 供应链风险 :还记得
event-stream投毒事件吗?依赖越少,攻击面越小 - 版本冲突:包 A 依赖 lodash@4,包 B 依赖 lodash@3,解决冲突的时间比写代码还长
2026 年的浏览器已经非常强大了。很多你以为"必须装包"的功能,原生 API 早就支持了。
1. 卸载 lodash.cloneDeep → 用 structuredClone()
之前:
bash
npm install lodash.cloneDeep # 5.3KB gzip
javascript
import cloneDeep from 'lodash.cloneDeep';
const copy = cloneDeep(complexObject);
现在:
javascript
const copy = structuredClone(complexObject);
完了。一行,零依赖。
structuredClone 是浏览器原生的深拷贝方法,支持 Map、Set、Date、RegExp、ArrayBuffer、循环引用------这些 JSON.parse(JSON.stringify()) 做不到的,它全能做。
兼容性: Chrome 98+、Firefox 94+、Safari 15.4+,2026 年你不需要担心兼容性。
唯一限制: 不支持拷贝 DOM 节点和函数。如果你的对象里有函数属性,这个方案不适用。但说实话,你的数据对象里不应该有函数。
能省多少: lodash.cloneDeep 单独引入约 5.3KB gzip,卸载后直接省掉。
2. 卸载 uuid → 用 crypto.randomUUID()
之前:
bash
npm install uuid # 2.7KB gzip
javascript
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
现在:
javascript
const id = crypto.randomUUID(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
输出格式完全一样,都是标准的 UUID v4。
兼容性: Chrome 92+、Firefox 95+、Safari 15.4+,全线支持。
Node.js 也支持: Node 19+ 内置 crypto.randomUUID(),前后端通吃。
如果你只需要一个唯一 ID 而不需要严格的 UUID 格式,还有更轻量的方案:
javascript
const simpleId = Math.random().toString(36).slice(2, 11);
// '5x3g7k9m2'
3. 卸载 dayjs / moment → 用 Intl.DateTimeFormat + Temporal
这个是最重磅的。moment.js 光 gzip 就 72KB,dayjs 虽然轻(2KB),但大多数场景你连 2KB 都不需要。
场景一:格式化日期显示
javascript
// 之前:dayjs
import dayjs from 'dayjs';
dayjs(date).format('YYYY年MM月DD日');
// 现在:原生 Intl
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
// '2026/06/21'
场景二:相对时间("3 小时前")
javascript
// 之前:dayjs + relativeTime 插件
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs(date).fromNow();
// 现在:原生 Intl.RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
rtf.format(-3, 'hour'); // '3小时前'
rtf.format(-1, 'day'); // '昨天'
rtf.format(2, 'month'); // '后2个月'
场景三:日期计算
javascript
// 之前
dayjs().add(7, 'day').toDate();
// 现在:Temporal API(2026 年主流浏览器已支持)
const now = Temporal.Now.plainDateISO();
const nextWeek = now.add({ days: 7 });
console.log(nextWeek.toString()); // '2026-06-28'
什么时候还需要 dayjs: 如果你要做大量复杂的时区转换、日历系统切换(农历之类),dayjs 的插件生态还是有价值的。但如果只是格式化显示和简单计算,原生足够了。
4. 卸载 classnames / clsx → 用模板字符串
之前:
bash
npm install classnames # 0.6KB gzip
javascript
import cn from 'classnames';
<div className={cn('btn', {
'btn-primary': isPrimary,
'btn-disabled': isDisabled,
'btn-large': size === 'large'
})} />
现在:
javascript
<div className={[
'btn',
isPrimary && 'btn-primary',
isDisabled && 'btn-disabled',
size === 'large' && 'btn-large',
].filter(Boolean).join(' ')} />
如果你觉得 filter(Boolean).join(' ') 写起来啰嗦,封装一个两行的工具函数:
javascript
const cn = (...args) => args.filter(Boolean).join(' ');
// 用法完全一样
<div className={cn(
'btn',
isPrimary && 'btn-primary',
isDisabled && 'btn-disabled',
)} />
2 行代码替代一个 npm 包。
更好的方案: 如果项目用了 Tailwind CSS,tailwind-merge 比 classnames 更合适,因为它能处理 Tailwind 的类名冲突(比如同时写了 p-2 和 p-4)。这种场景下原生方案做不到。
5. 卸载 node-fetch → 用原生 fetch
之前(Node.js 环境):
bash
npm install node-fetch # 8.4KB gzip
javascript
import fetch from 'node-fetch';
const res = await fetch('https://api.example.com/data');
现在:
javascript
const res = await fetch('https://api.example.com/data');
Node.js 18+ 内置了 fetch,不需要再装 node-fetch。2026 年还在装这个包,大概率是因为 package.json 里一直没清理。
注意: 如果你的 Node.js 版本低于 18,还是需要 node-fetch。但 2026 年了,Node 18 已经是 EOL,你至少应该在 Node 20+ 上。
6. 卸载 qs → 用 URLSearchParams
之前:
bash
npm install qs # 6.2KB gzip
javascript
import qs from 'qs';
// 序列化
const query = qs.stringify({ page: 1, size: 20, keyword: '前端' });
// 'page=1&size=20&keyword=%E5%89%8D%E7%AB%AF'
// 解析
const params = qs.parse('page=1&size=20');
// { page: '1', size: '20' }
现在:
javascript
// 序列化
const query = new URLSearchParams({ page: 1, size: 20, keyword: '前端' }).toString();
// 'page=1&size=20&keyword=%E5%89%8D%E7%AB%AF'
// 解析
const params = Object.fromEntries(new URLSearchParams('page=1&size=20'));
// { page: '1', size: '20' }
唯一限制: URLSearchParams 不支持嵌套对象和数组的序列化。如果你的查询参数是 { filter: { status: ['active', 'pending'] } } 这种结构,还是需要 qs。但大多数前端场景的查询参数都是扁平的 key-value。
实操:怎么找出项目里可以卸载的包
第一步:找出未使用的依赖
bash
npx depcheck
它会列出 package.json 里声明了但代码里从未 import 的包,直接删。
第二步:分析打包体积
bash
npx vite-bundle-visualizer
# 或 webpack 项目
npx webpack-bundle-analyzer
看看哪些包占了大头。通常 moment、lodash 完整包是体积杀手。
第三步:逐个替换
按本文方案替换后,跑一遍测试,确认功能正常再发版。
总结对照表
| npm 包 | gzip 体积 | 原生替代 | 限制 |
|---|---|---|---|
| lodash.cloneDeep | 5.3KB | structuredClone() |
不支持函数和 DOM |
| uuid | 2.7KB | crypto.randomUUID() |
无 |
| dayjs | 2KB | Intl + Temporal |
复杂时区场景不够用 |
| classnames | 0.6KB | filter(Boolean).join(' ') |
无 |
| node-fetch | 8.4KB | 原生 fetch (Node 18+) |
需要 Node 18+ |
| qs | 6.2KB | URLSearchParams |
不支持嵌套对象 |
| 合计 | ~25KB | 0KB | --- |
25KB gzip 看起来不多,但在移动端弱网环境下,这就是 200-500ms 的加载时间差距。
更重要的是:少一个依赖,就少一个供应链攻击的入口,少一个版本冲突的可能,少一个 npm audit 的告警。
如果你的项目里还有其他可以用原生 API 替代的包,评论区说一下,我补充进来。点赞收藏一下,下次 Code Review 看到同事装多余的包,直接把这篇甩给他。