聊天讨论 2026 年了,这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代

193577746(kyriewen) · 2026年06月21日 · 18 次阅读

前几天我跑了一下 npx depcheck,发现项目里有 47 个依赖,其中至少 6 个完全可以用浏览器原生 API 替代。卸载之后,打包体积直接少了 82KB(gzip 后少了 23KB),首屏加载快了 300ms。这篇文章把每个包的原生替代方案都写出来,附迁移代码,直接抄。

为什么要清理依赖

每多一个 npm 包,你的项目就多了:

  • 打包体积:用户每次访问都要多下载这些代码
  • 供应链风险:还记得 event-stream 投毒事件吗?依赖越少,攻击面越小
  • 版本冲突:包 A 依赖 lodash@4,包 B 依赖 lodash@3,解决冲突的时间比写代码还长

2026 年的浏览器已经非常强大了。很多你以为"必须装包"的功能,原生 API 早就支持了。


1. 卸载 lodash.cloneDeep → 用 structuredClone()

之前:

npm install lodash.cloneDeep   # 5.3KB gzip
import cloneDeep from 'lodash.cloneDeep';

const copy = cloneDeep(complexObject);

现在:

const copy = structuredClone(complexObject);

完了。一行,零依赖。

structuredClone 是浏览器原生的深拷贝方法,支持 MapSetDateRegExpArrayBuffer、循环引用——这些 JSON.parse(JSON.stringify()) 做不到的,它全能做。

兼容性: Chrome 98+、Firefox 94+、Safari 15.4+,2026 年你不需要担心兼容性。

唯一限制: 不支持拷贝 DOM 节点和函数。如果你的对象里有函数属性,这个方案不适用。但说实话,你的数据对象里不应该有函数。

能省多少: lodash.cloneDeep 单独引入约 5.3KB gzip,卸载后直接省掉。


2. 卸载 uuid → 用 crypto.randomUUID()

之前:

npm install uuid   # 2.7KB gzip
import { v4 as uuidv4 } from 'uuid';

const id = uuidv4(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

现在:

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 格式,还有更轻量的方案:

const simpleId = Math.random().toString(36).slice(2, 11);
// '5x3g7k9m2'

3. 卸载 dayjs / moment → 用 Intl.DateTimeFormat + Temporal

这个是最重磅的。moment.js 光 gzip 就 72KB,dayjs 虽然轻(2KB),但大多数场景你连 2KB 都不需要。

场景一:格式化日期显示

// 之前: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 小时前")

// 之前: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个月'

场景三:日期计算

// 之前
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 → 用模板字符串

之前:

npm install classnames   # 0.6KB gzip
import cn from 'classnames';

<div className={cn('btn', {
  'btn-primary': isPrimary,
  'btn-disabled': isDisabled,
  'btn-large': size === 'large'
})} />

现在:

<div className={[
  'btn',
  isPrimary && 'btn-primary',
  isDisabled && 'btn-disabled',
  size === 'large' && 'btn-large',
].filter(Boolean).join(' ')} />

如果你觉得 filter(Boolean).join(' ') 写起来啰嗦,封装一个两行的工具函数:

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-2p-4)。这种场景下原生方案做不到。


5. 卸载 node-fetch → 用原生 fetch

之前(Node.js 环境):

npm install node-fetch   # 8.4KB gzip
import fetch from 'node-fetch';
const res = await fetch('https://api.example.com/data');

现在:

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

之前:

npm install qs   # 6.2KB gzip
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' }

现在:

// 序列化
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。


实操:怎么找出项目里可以卸载的包

第一步:找出未使用的依赖

npx depcheck

它会列出 package.json 里声明了但代码里从未 import 的包,直接删。

第二步:分析打包体积

npx vite-bundle-visualizer
# 或 webpack 项目
npx webpack-bundle-analyzer

看看哪些包占了大头。通常 momentlodash 完整包是体积杀手。

第三步:逐个替换

按本文方案替换后,跑一遍测试,确认功能正常再发版。


总结对照表

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 看到同事装多余的包,直接把这篇甩给他。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号