用 Baseline 减少 JavaScript 依赖:审计 package.json 中浏览器已原生支持的库
Smashing Magazine 发表技术文章,由独立 JavaScript 与 Web 性能顾问 Jad Joubran 撰写,详细介绍了如何通过审计项目依赖,发现并移除那些浏览器已经原生支持的 JavaScript 库,从而减少打包体积、提升性能。文章指出,"你需要一个库来做这个"和"浏览器已经能做这个"之间的差距正在不断缩小。在一个典型的中等规模 JavaScript 应用中,通常可以找到 60KB 到 90KB(minified + gzipped)的依赖是浏览器平台现在已经可以自行处理的。日期和数字格式化、HTTP 请求、模态框、工具提示、深拷贝、数组合组——这些在几年前都是真正的功能缺口,现在很多已经不是了。
为什么依赖会累积
安装后不再审视
大多数开发者的经历:
- 安装一个依赖来解决某个问题
- 它能工作,测试通过
- 然后继续做其他事情
- 再也不回头看这个依赖
但 Web 平台在不断发展:
- 浏览器每年发布多个新版本
- 新的 Web API 不断标准化
- 曾经需要库的功能现在可能已经原生支持
- 库的维护状态可能已经变化
缺乏定期审计
大多数团队:
- 会运行
npm audit检查安全漏洞 - 但很少问"这个库现在还在做浏览器做不到的事情吗?"
- 没有定期的依赖审计流程
- 不知道浏览器平台发展有多快
结果就是:库留在了 package.json 中,即使它们已经不再必要。
可被原生替代的常见依赖类别
1. 日期和时间格式化
常见库:moment.js、date-fns、dayjs
原生替代:Intl.DateTimeFormat
// 以前需要 moment.js
const moment = require('moment');
moment(date).format('YYYY-MM-DD');
// 现在可以用 Intl.DateTimeFormat
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(date);
Intl.DateTimeFormat 支持:
- 多种语言和地区
- 自定义日期和时间格式
- 相对时间格式化(
Intl.RelativeTimeFormat) - 性能优于大多数库
注意:如果需要复杂的日期运算(如加减、比较、时区处理),date-fns 或 dayjs 仍然有价值。但如果只是格式化,原生 API 足够。
2. 数字格式化
常见库:numeral.js、accounting.js
原生替代:Intl.NumberFormat
// 货币格式化
new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(1234.56); // ¥1,234.56
// 百分比
new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 1
}).format(0.123); // 12.3%
// 大数字缩写
new Intl.NumberFormat('en', {
notation: 'compact',
compactDisplay: 'short'
}).format(1234567); // 1.2M
3. HTTP 请求
常见库:axios、request、superagent
原生替代:fetch API(现在所有现代浏览器和 Node.js 18+ 都支持)
// GET 请求
const response = await fetch('/api/users');
const data = await response.json();
// POST 请求
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' })
});
fetch 的优势:
- 原生支持,无需额外依赖
- 基于 Promise,支持 async/await
- 支持流式处理(Streams API)
- 支持 AbortController 取消请求
axios 仍然有价值的场景:
- 需要请求/响应拦截器
- 需要自动 JSON 转换
- 需要超时处理
- 需要上传进度
- 需要广泛的旧浏览器支持
但对于简单的 HTTP 请求,fetch 已经足够。
4. 深拷贝
常见库:lodash.cloneDeep、clone
原生替代:structuredClone
// 以前需要 lodash
const clone = _.cloneDeep(original);
// 现在可以用 structuredClone
const clone = structuredClone(original);
structuredClone 支持:
- 嵌套对象和数组
- Date、RegExp、Map、Set
- ArrayBuffer、TypedArray
- 循环引用
不支持:
- Function
- DOM 节点
- 属性描述符(getter/setter)
- 原型链(会丢失原型)
对于大多数数据对象的深拷贝,structuredClone 是完美的原生替代。
5. 数组合组和操作
常见库:lodash.groupBy、lodash.uniq、lodash.orderBy
原生替代:Object.groupBy、Map.groupBy、Set、sort
// 分组(ES2024)
const grouped = Object.groupBy(users, user => user.role);
// 去重
const unique = [...new Set(items)];
// 排序
const sorted = [...items].sort((a, b) => a.age - b.age);
6. 模态框和对话框
常见库:各种 Modal 组件库
原生替代:<dialog> 元素
<dialog id="myDialog">
<h2>对话框标题</h2>
<p>对话框内容</p>
<button onclick="document.getElementById('myDialog').close()">关闭</button>
</dialog>
<button onclick="document.getElementById('myDialog').showModal()">打开</button>
<dialog> 元素的优势:
- 原生支持,无需 JavaScript 库
- 内置焦点管理
- 内置模态行为(点击遮罩关闭、ESC 关闭)
- 无障碍支持
- 可以用 CSS
::backdrop自定义遮罩
7. 工具提示(Tooltip)
常见库:tippy.js、tooltip.js
原生替代:CSS :hover + title 属性,或 Popover API
<!-- 简单的 title 属性 -->
<button title="这是提示信息">按钮</button>
<!-- Popover API(更现代) -->
<button popovertarget="myPopover">打开提示</button>
<div id="myPopover" popover>提示内容</div>
8. URL 解析和操作
常见库:query-string、url-parse
原生替代:URL 和 URLSearchParams
// 解析 URL
const url = new URL('https://example.com/path?query=value');
console.log(url.pathname); // /path
console.log(url.searchParams.get('query')); // value
// 操作查询参数
const params = new URLSearchParams();
params.append('key', 'value');
console.log(params.toString()); // key=value
9. 防抖和节流
常见库:lodash.debounce、lodash.throttle
原生替代:自己实现(代码很短)
// 防抖
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 节流
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
这两个函数总共不到 20 行代码,不需要引入整个 lodash。
10. 唯一 ID 生成
常见库:uuid、nanoid
原生替代:crypto.randomUUID()
// 生成 UUID
const id = crypto.randomUUID();
console.log(id); // "550e8400-e29b-41d4-a716-446655440000"
crypto.randomUUID() 在所有现代浏览器和 Node.js 19+ 中都可用。
依赖审计的方法
1. 按类别审计
不要一个一个地检查依赖,而是按类别分组审计:
- 日期和时间处理
- HTTP 请求
- 工具函数(数组、对象操作)
- UI 组件(模态框、提示、下拉菜单)
- 加密和安全
- 开发工具(构建、测试、lint)
因为可替代的依赖往往成组出现。
2. 分析打包体积
使用工具分析打包体积:
webpack-bundle-analyzersource-map-explorerrollup-plugin-visualizer- Vite 的
rollup-plugin-visualizer
找出占用体积最大的依赖,评估是否可以替代。
3. 检查浏览器兼容性
在决定移除库之前,检查原生 API 的浏览器兼容性:
- Can I Use(caniuse.com)
- MDN Web Docs
- 项目的目标浏览器列表
如果需要支持旧浏览器,可以考虑:
- 使用 polyfill(通常比完整库小)
- 渐进增强(新浏览器用原生,旧浏览器用库)
- 调整目标浏览器列表
4. 渐进式迁移
不要一次性移除所有可替代的依赖:
- 选择一个类别开始
- 替换代码并测试
- 确认没有回归问题
- 移除依赖
- 继续下一个类别
5. 建立持续审计机制
- 将依赖审计加入代码审查流程
- 定期(如每季度)运行依赖审计
- 使用工具自动检测可替代的依赖
- 在 CI 中检查打包体积变化
性能收益估算
文章指出,在典型的中等规模 JavaScript 应用中:
- 可以找到 60KB 到 90KB(minified + gzipped)的可替代依赖
- 这相当于减少 20-30% 的 JavaScript 体积
- 可以带来明显的性能提升:
- 更快的页面加载
- 更少的网络传输
- 更少的 JavaScript 解析和编译时间
- 更低的内存使用
- 更好的用户体验
对于移动端和低速网络用户,这些收益更加明显。
注意事项
不要盲目移除
在移除库之前,确保:
- 原生 API 确实覆盖了库的所有功能
- 项目的目标浏览器支持原生 API
- 有足够的测试覆盖
- 团队成员熟悉原生 API
有些库仍然有价值
以下场景库仍然有价值:
- 复杂的日期运算(不只是格式化)
- 高级 HTTP 功能(拦截器、重试、缓存)
- 复杂的 UI 组件(数据表格、富文本编辑器)
- 跨浏览器兼容性要求高
- 团队不熟悉原生 API
- 库提供了原生 API 没有的高级功能
考虑维护成本
移除库也有成本:
- 需要编写和维护自己的实现
- 团队需要学习原生 API
- 可能需要处理边缘情况
- 未来可能需要重新引入库
但对于简单、标准化的功能,原生 API 的维护成本通常低于第三方库。
总结
Web 平台在快速发展,许多曾经需要第三方库的功能现在已经被浏览器原生支持。通过定期审计项目依赖,可以发现并移除这些不再必要的库,减少打包体积、提升性能。常见的可替代类别包括日期格式化(Intl.DateTimeFormat)、数字格式化(Intl.NumberFormat)、HTTP 请求(fetch)、深拷贝(structuredClone)、数组合组(Object.groupBy)、模态框(dialog 元素)、URL 解析(URL API)、防抖节流(自行实现)、唯一 ID(crypto.randomUUID)等。在典型的中等规模应用中,可以减少 60-90KB(gzipped)的依赖,带来明显的性能提升。但不要盲目移除,需要评估原生 API 的功能覆盖、浏览器兼容性和维护成本。建立持续的依赖审计机制,定期审视项目中的依赖,是保持应用轻量和高效的重要实践。随着 Web 平台的持续发展,"你需要一个库来做这个"和"浏览器已经能做这个"之间的差距将继续缩小,开发者应该保持对平台新能力的关注,适时优化项目依赖。
来源:https://www.smashingmagazine.com/2026/08/how-baseline-can-help-you-ship-less-javascript/