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

193577746(kyriewen) · June 21, 2026 · 18 hits

前几天我跑了一下 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 看到同事装多余的包,直接把这篇甩给他。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.