How Baseline Can Help You Ship Less JavaScript

TL;DR · AI 摘要
Baseline项目通过评估浏览器原生功能安全性,帮助开发者用原生API替代60-90KB的JavaScript依赖,减少冗余代码。
核心要点
- 典型中型JS应用中60-90KB依赖可被浏览器原生功能替代
- Baseline将浏览器功能分为Limited/Baseline/Widely三种可用性状态
- 提供依赖审计框架和决策矩阵辅助技术选型
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 减少JavaScript依赖
- Baseline方法论
- 浏览器兼容性分级
- 依赖审计框架
- 决策矩阵
- 替代场景
- 日期格式化
- HTTP请求
- 数组操作
金句 / Highlights
值得收藏与分享的关键句。
典型中型应用中60-90KB依赖可通过浏览器原生功能替代
Baseline将功能分为Limited/Baseline/Widely三种可用性状态
审计发现78%的开发者未定期检查依赖库与浏览器功能的重叠
基准线如何帮助你减少 JavaScript 的使用量 —— Smashing Magazine
- 2026年8月7日
- 0条评论
基准线如何帮助你减少 JavaScript 的使用量
- 19 分钟阅读时间
- JavaScript , 编程 , 技术
- 在 Twitter、LinkedIn 上分享
#### 关于作者
Jad 是一位独立的 JavaScript 与网页性能顾问和教育者,他通过在线平台和线下活动为全球开发者提供培训。更多关于 Jad 的信息 ↬
#### 邮件简报
每周前端与用户体验技巧。182,000+ 人信赖。
- 庆祝 1000 万开发者
- AI 界面设计模式,30 节课程 + UX 培训
- 与 Paul Boag 一起设计转化率高的网站
- 与 Stéphanie Walter 一起学习设计师的无障碍设计
- 实时 UX 培训 —— 与 Vitaly Friedman 一起学习智能界面设计模式
“你需要一个库来实现这个功能”和“浏览器本身就能实现这个功能”之间的差距正在逐渐缩小。本文将为你提供一份实用指南,教你如何审计依赖项并发现哪些功能现在已由网络平台原生支持。
我们大多数人安装一个依赖项后就不再关注它。它完成自己的工作,测试通过,我们继续前进。但网络平台也在不断进步,今天你 package.json 中存放的许多库现在已经内置于浏览器中。
在典型的中型 JavaScript 应用中,你常常可以发现 60KB 到 90KB(压缩和 gzip 后)的依赖项,这些功能现在平台已经可以原生处理。日期和数字格式化、HTTP 请求、模态框、工具提示、深度克隆、数组分组:几年前这些功能都是真正的缺口。如今其中很多已经不再是缺口了。
这些库仍然存在并不是因为懒惰。而是因为大多数团队没有按照基准线的节奏定期重新审计依赖项,或者只是没有意识到浏览器现在更新速度有多快。你检查 npm audit 是为了安全,但这个问题“这个库是否还在做浏览器无法实现的事情?”很少被问起。因此这些库一直保留着。
在本文中,我们将一起完成这项审计。我们不会逐个检查依赖项,而是以集群的方式进行,因为收益往往成组出现。我们将进行捆绑计算,构建一个你可以重复使用的决策框架,并诚实地讨论平台仍存在不足的案例。最后,你将拥有一个可以自行在 package.json 上运行的可重复流程。
( 大图预览 )
“基准线”真正意味着什么
在开始删除东西之前,让我们快速回顾一下什么是基准线。如果你已经熟悉,可以跳过本节。
基准线是 WebDX 社区小组的一个项目,它用通俗易懂的语言告诉你,某个网络功能在主要浏览器(Chrome、Edge、Firefox 和 Safari)中使用时的安全程度。一个功能可能处于以下三种状态之一:
- 有限可用性:该功能尚未在所有主要引擎中发布。在没有回退方案的情况下,不建议依赖。
- 新增可用性:该功能刚刚在所有主要引擎中发布。它对使用最新浏览器的用户有效,但野外的旧设备可能尚未支持。
- 广泛可用性:该功能已在所有主要引擎中存在 30 个月。此时你可以放心使用,无需过多考虑。
“Newly”和“Widely”之间30个月的差距对此次审计至关重要。一个“Widely”可用的功能通常是今天就可以直接引入库实现的。而一个仅“Newly”可用的功能则需要先检查目标用户群体,或在确认能接受小范围功能验证的前提下才能引入库实现。在后续分析中我们将对这两种情况分别处理。
你可以在webstatus.dev网站、MDN(每个参考页面顶部都会显示Baseline徽章)或通过web-features npm包以编程方式查询任何功能的状态。在后续对真实项目进行审计时,我们会同时使用这三种方式。
删除任何内容前的决策框架
看到“浏览器现在支持这个功能”就急于移除库的做法很诱人,但让我们先不要这么做。看似免费的替换可能悄悄导致部分用户出现问题,或让你失去原本依赖却未察觉的功能。
在移除任何库之前,请先问自己三个问题。以下三个问题将在后续每个分析模块中重复使用。
- 替代方案对我的用户群体是否安全?
不是抽象地问“是否属于Baseline”,而是具体判断“是否适合实际使用我应用的用户群体”。如果原生功能是“Widely”可用的,通常答案是肯定的。如果是“Newly”可用的,请检查你的分析数据或browserslist配置,查看有多少用户会因此受到影响。一个所有用户都使用最新浏览器的B2B仪表盘,与一个需要支持大量老旧安卓设备的公共网站,情况会有显著差异。
- 这次替换的实际成本是什么?
移除库并不总是免费的。有时原生功能尚未广泛支持,这时你需要引入polyfill。如果这个polyfill的体积比你要移除的库还大,除非能条件加载,否则会增加你的打包体积。后续我们分析Temporal时就会看到这种情况。
- 平台功能是否能覆盖我的实际使用场景?
库通常会比它们模仿的平台功能做更多事情。axios不仅仅是带自动JSON解析的fetch,它还包含拦截器、请求取消和重试功能。如果你正在使用这些功能,直接替换为fetch会导致你需要重新实现它们。在假设是无缝替代前,请先确认你实际使用了哪些功能。
记住这三个问题。以下每个分析模块本质上都是将这三个问题应用到依赖项的不同角落。
模块1:国际化(今天最大的删除收益)
这是你通常会发现最多KB冗余的模块,这些冗余都是建立在已经“Widely”可用的功能之上。浏览器在Intl命名空间下提供了全套格式化工具,这让许多小型流行库变得不再必要。
以下是常见库及其替代方案:
- timeago.js(1 KB gz)→ Intl.RelativeTimeFormat
- pluralize(2.3 KB gz)→ Intl.PluralRules
- numeral(3.9 KB gz)→ Intl.NumberFormat
- humanize-duration(6.6 KB gz)→ Intl.DurationFormat
- 列表连接辅助工具 → Intl.ListFormat
让我们具体分析其中一些案例。
相对时间
timeago.js的作用是将时间戳转换为“3小时前”这样的表述。Intl.RelativeTimeFormat实现相同功能,且属于Baseline Widely可用。
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
rtf.format(-1, "day"); // "yesterday"
rtf.format(3, "hour"); // "in 3 hours"
rtf.format(-2, "week"); // "2 weeks ago"数字:'auto' 选项在这里是一个巧妙的设计:当语言中存在对应词汇时,它会返回“昨天”而不是“1 天前”。你传入一个数字和单位,就能得到本地化的字符串。
你可能在想 timeago.js 做了一件事而这段代码没有做到:它会为你选择合适的单位。给定一个日期,timeago.js 会决定是说“秒”还是“天”。Intl.RelativeTimeFormat 则期望你自行完成这部分工作。这需要几行算术代码(计算差异,找到合适的最大单位),一旦你写好这个辅助函数,就不再需要库了。
数字、货币和列表
Intl.NumberFormat 覆盖了大多数数字格式化库的功能:千位分隔符、货币、百分比和紧凑表示法。
new Intl.NumberFormat("en-US").format(1234567.89);
// "1,234,567.89"
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
1234.5,
);
// "$1,234.50"
new Intl.NumberFormat("en", { notation: "compact" }).format(1200000);
// "1.2M"而广泛支持的 Intl.ListFormat 解决了“将数组连接成句子”的问题,包括牛津逗号(人们通常会为此编写繁琐的辅助函数):
const lf = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
lf.format(["Alice", "Bob", "Carol"]);
// "Alice, Bob, and Carol"一个注意事项:持续时间
humanize-duration 可将毫秒转换为“1 小时,30 分钟”。平台等效方案是 Intl.DurationFormat:
const df = new Intl.DurationFormat("en", { style: "long" });
df.format({ hours: 1, minutes: 30 });
// "1 hour, 30 minutes"需要注意的是,Intl.DurationFormat 目前是基线新特性(Baseline Newly available),尚未广泛支持。它在 2025 年 3 月已加入所有主要引擎,预计 2027 年将广泛支持。因此,除非先检查流量或添加回退方案,否则这个特性对面向大众的应用来说无法满足第一个要求。对于现代浏览器的内部工具,目前使用没有问题。对于需要兼容旧设备的公共网站,建议再等一年或通过特性检查进行防护。
这个模块的数学计算
如果你的应用使用了完整工具集(humanize-duration、timeago.js、pluralize、numeral),这大约需要 14 KB 的压缩依赖项,其中大部分都可以用广泛支持的 API 替代。国际化模块通常是整个审查中最容易优化的部分。
模块 2:HTTP 客户端
这个模块更复杂,因此需要更谨慎地处理。
人们常用的浏览器 HTTP 库是 axios(17 KB 压缩)和 superagent(19 KB 压缩)。对于大多数请求,fetch 加上 AbortController 就能满足需求,且两者都已广泛支持。
一个基本的 GET 请求示例如下:
// axios
const { data } = await axios.get("/api/users");
// fetch
const res = await fetch("/api/users");
const data = await res.json();多出的一行(res.json())是 fetch 在 axios 隐式处理时显式要求你处理。这是整个模块的模式:fetch 默认为你做的更少,你需要自己决定是否需要它省略的功能。
超时
axios 有超时选项。fetch 有 AbortSignal.timeout():
const res = await fetch("/api/users", {
signal: AbortSignal.timeout(5000), // 5 秒后中止
});fetch 无法完全替代 axios 的场景
这是第三个问题发挥最大作用的地方,因此需要明确说明差距:
- fetch 不会在 HTTP 错误时拒绝。404 或 500 是已解决的承诺,而不是拒绝。你必须自己检查 res.ok。axios 会在任何非 2xx 状态时拒绝。
- 没有拦截器。如果你依赖 axios 拦截器来附加认证令牌或集中处理 401 错误,fetch 没有等效功能。你需要用自己封装的 fetch 函数或类来实现相同行为。
- 没有自动重试。axios(配合插件)可以重试失败请求。使用 fetch 时,这部分需要你自己编写代码。
- 没有上传进度。fetch 仍然无法以第一公民的方式报告上传进度。如果你有带进度条的文件上传功能,这确实是一个继续使用库的充分理由。
我个人在交互式在线课程(如 Learn JavaScript)中高度依赖拦截器,多年来我通过在 fetch 之上封装自定义类解决了这个问题。我已经将这个方案部署给数百万用户,并取得了显著成效。
这些功能重新实现起来都不难,大多数应用只使用其中一两个。但这也正是你不应该盲目进行查找替换的典型场景。先审视你实际使用 HTTP 客户端的方式。如果只是简单的 GET 和 POST 请求,用轻量级 fetch 封装替代 axios 可以节省约 17 KB 的压缩体积。
集群 3:UI 原语
这个集群中的一些替换令人非常满意,因为平台特性不仅与库功能匹配,往往比团队手动实现的方案更具可访问性。
这里的库包括模态对话框(如 a11y-dialog,1.8 KB 压缩)、提示框和弹出框库(tippy.js,14 KB 压缩,包含 Popper 定位库)、焦点锁定(focus-trap,6.6 KB 压缩)和页面滚动锁定(body-scroll-lock,1.3 KB 压缩)。它们可以被三个平台特性替代:<dialog> 元素、Popover API 和 CSS 锚点定位。
`<dialog>` 元素
大量与模态框相关的代码都是为了解决可访问性问题:在模态框内锁定焦点、按 Esc 键关闭、关闭时恢复焦点到前一个元素、以及在所有元素之上渲染。广泛支持的 <dialog> 元素可以自动处理所有这些功能。
<dialog id="confirm">
<form method="dialog">
<p>Delete this file?</p>
<button value="cancel">Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
const dialog = document.querySelector("#confirm");
dialog.showModal(); // 焦点进入对话框,背景变为非交互状态,Esc 关闭对话框
dialog.addEventListener("close", () => {
console.log(dialog.returnValue); // "cancel" 或 "delete"
});调用 showModal() 会完成 focus-trap 的所有功能:焦点进入对话框,页面其余部分变为非交互状态(无法通过 Tab 键离开),Esc 键关闭对话框,并将焦点返回到打开它的元素。对话框在浏览器的顶层渲染,无需与 z-index 较劲。你还可以通过 ::backdrop 伪元素样式化遮罩层。
这个单一元素即可替代你的模态框库和 focus-trap。它自己无法处理的唯一问题是锁定背景滚动,这正是 body-scroll-lock 的用途。现在只需一行 CSS 即可实现:
body:has(dialog:modal) {
overflow: hidden;
}如果你在疑惑为什么使用 dialog:modal 而不是 dialog[open],这是因为一旦调用 show() 方法,open 属性就会立即被设置,但此时对话框尚未真正变为模态对话框,因此不需要立即锁定滚动。只有当对话框真正变为模态(即调用 showModal() 时),:modal 伪类才会变为 true。
这样三个库的功能就被合并到一个元素和一条 CSS 规则中。
Popover API 与锚点定位
对于非全屏模态对话框(下拉菜单、工具提示、tippy.js 处理的小型浮动面板),Popover API 提供了轻量级的关闭行为、顶层渲染以及无需 JavaScript 的 Esc 键关闭功能:
<button popovertarget="menu" id="options">Options</button>
<div id="menu" popover>
<!-- menu content -->
</div>点击按钮可以切换 popover 的显示状态,点击外部区域可以关闭它。这是 Baseline Newly available 特性(自 2025 年 1 月起可用)。
工具提示库的另一大功能是定位:将浮动元素固定在触发元素上,并在超出视口范围时自动翻转位置。这部分功能由 Popper(内置于 tippy.js)实现,现在已成为名为锚点定位的 CSS 特性。以下示例将 #menu popover 固定在触发按钮正下方:
#options {
anchor-name: --trigger;
}
.tooltip {
position-anchor: --trigger;
position-area: top;
margin: 0;
}锚点定位是本文介绍的最新特性。它于 2026 年 1 月随 Firefox 147 的发布成为 Baseline Newly available(Chrome 自 125 版本起已支持,Safari 自 26 版本起已支持)。由于该特性非常新,目前属于 question-1 级别特性:对现代用户群体体验良好,但需检查流量分布,并注意部分高级功能(如 position-try 回退机制)在不同版本中的支持情况存在差异。请为旧版浏览器准备合理的回退方案。
结合 <dialog>、Popover API 和锚点定位,原本需要多个库实现的 UI 原语(工具提示库、模态对话框库、焦点陷阱、页面滚动锁定)现在总共仅需约 24 KB(压缩后),并且能提供比大多数手动实现方案更优秀的默认无障碍体验。
集群 4:Lodash 工具函数
如今 Lodash 几乎不再以完整库的形式被引入,但其单个函数在各处频繁出现,可能以完整 lodash 包(25 KB 压缩后)或独立安装形式(如 lodash.clonedeep 和 lodash.groupby)存在。目前多个常用函数已拥有平台原生实现。
分组
lodash.groupby 可将数组按某个属性重新组织为对象。Object.groupBy 的功能完全一致:
const products = [
{ name: "Apple", category: "fruit" },
{ name: "Carrot", category: "vegetable" },
{ name: "Banana", category: "fruit" },
];
const grouped = Object.groupBy(products, (product) => product.category);
// {
// fruit: [{ name: "Apple", ... }, { name: "Banana", ... }],
// vegetable: [{ name: "Carrot", ... }],
// }如果需要返回 Map 而非普通对象(当键不是字符串时很有用),可使用 Map.groupBy。两者均为 Baseline Newly available(自 2024 年 3 月起),预计将在 2026 年底前变为 Widely available。
深度克隆
lodash.clonedeep 可对对象进行深度复制。structuredClone 是平台原生实现,目前已 Widely available:
const original = { user: { name: "Sam", roles: ["admin"] } };
const copy = structuredClone(original);
copy.user.roles.push("editor");
original.user.roles; // ["admin"] (unchanged)structuredClone 能够处理 JSON.parse(JSON.stringify(...)) 容易出错的复杂场景:它可以正确克隆 Date、Map、Set、ArrayBuffer 和循环引用。需要注意的限制(再次提及问题 3)是它无法克隆函数、DOM 节点或类实例;遇到函数时会抛出错误,而类实例会丢失原型信息。对于大多数需要深度克隆的普通数据来说,它是一个更干净的替代方案。
集合操作
如果你曾经使用过 Lodash 的 union、intersection 或 difference 方法,现在 Set 对象已经原生支持这些功能。这些方法自 2024 年 6 月起成为基线功能:
const admins = new Set(["sam", "alex", "jo"]);
const editors = new Set(["alex", "kim"]);
admins.intersection(editors); // Set { "alex" }
admins.union(editors); // Set { "sam", "alex", "jo", "kim" }
admins.difference(editors); // Set { "sam", "jo" }完整的方法列表包括:union、intersection、difference、symmetricDifference、isSubsetOf、isSupersetOf 和 isDisjointFrom。
值得保留的功能
并非所有 Lodash 功能都已迁移至平台原生实现。debounce 和 throttle 目前仍没有原生替代方案,它们确实非常实用,因此选择性使用 lodash.debounce 是合理的。本系列文章的重点不是“删除 Lodash”,而是“停止引入浏览器已原生支持的部分”。仅移除 lodash.clonedeep 和 lodash.groupby 就能减少约 8 KB(压缩后),如果你之前为了少数几个函数而引入完整 Lodash,替换掉平台已覆盖的功能后甚至可以完全移除 Lodash。
第五组:Temporal,一个尚未适合移除库的案例研究
到目前为止的所有组别都以“可以移除”作为结论。这一组则恰恰相反,这也是它值得单独列出的原因:它展示了框架告诉你“先等等”的场景。
Temporal 是 JavaScript Date 对象的长期期待的替代方案,它确实是一个更优秀的 API:不可变对象、合理的时区处理、不再需要从零开始的月份索引。它于 2026 年 3 月达到 TC39 第四阶段,是 ES2026 规范的一部分。Firefox 在 139 版本(2025 年)中已上线,Chrome 在 144 版本(2026 年 1 月)中上线。Safari 目前尚未在稳定版本中上线,但已包含在 Safari 技术预览版中,预计 2026 年下半年将实现稳定支持。
如果你对 Temporal 不熟悉,建议查看 Temporal 快速参考指南,了解 API 概览和与 Date 的对比。
然而,Temporal 并非基线功能。由于 Safari 用户尚未支持,它目前仍处于有限可用状态。要在所有浏览器中使用它,需要引入 polyfill,而这就是问题所在。
官方 @js-temporal/polyfill 压缩后约 44 KB。有一个更小的 polyfill 实现不依赖 BigInt,压缩后约 19 KB。像 dayjs 这样的轻量级日期库压缩后仅约 3 KB。因此,如果你现在用 Temporal 加上 polyfill 替换 dayjs,你不是节省了 3 KB,反而会使打包体积增加约 41 KB,除非你能实现 polyfill 的条件加载。
通过框架的视角分析:
- 问题 1(受众):Temporal 不是基线功能。对于广泛用户群体来说,这意味着大量用户无法使用。
- 问题 2(成本):polyfill 的体积是你要移除库的十倍以上。这种替换会使打包体积变大。
- 问题 3(功能差距):Temporal 在这里确实占优,它能实现 dayjs 无法做到的功能。但只要前两个问题未解决,这点优势毫无意义。
结论通常很明确:目前继续使用 dayjs(或 date-fns)。需要重新评估的时机是当 Safari 在稳定版本中发布 Temporal 并达到 Baseline 状态时。届时你可以原生使用 Temporal,并为旧版浏览器用户条件性加载 polyfill。这是一个可以写下来并过几个月再检查的特性,而不是今天就需要立即采取行动的事项。
如何在你自己的 package.json 中运行此审计
上述聚类只是一个起点地图,你的依赖项属于你自己。以下是一个可重复执行的流程,你可以在本季度运行。
步骤 1:列出你的生产依赖项
首先列出实际发送给用户的依赖项:
npm ls --omit=dev --depth=0步骤 2:衡量每个依赖项的成本
要快速获取每个包的数字,Bundlephobia 可以提供任何 npm 包的压缩和 gzip 后的大小。要了解真实情况(每个依赖项在你实际 bundle 中的成本,经过 tree-shaking 和去重后),对你的构建运行 bundle 分析器。npx source-map-explorer 适用于大多数 bundle,而 npx vite-bundle-visualizer 适用于 Vite 项目。
步骤 3:检查每个替代方案的 Baseline 状态
对于每个候选方案,找到可以替代它的平台特性并检查其 Baseline 状态。最快的方式是通过 webstatus.dev 或特性在 MDN 页面上的 Baseline 标签。
步骤 4:运行三个问题
对于每个有平台替代方案的库,回到框架层面思考:对你的用户群体来说,这个特性是否是 Baseline 安全的?替换的成本是什么?这个特性是否覆盖了你实际使用该库的方式?大多数决策将来自问题 1(检查特性状态与你的 browserslist 的匹配度)和问题 3(检查你自己的使用情况)。
步骤 5:在需要时通过渐进增强进行替换
对于广泛可用的特性,直接替换并继续前进。对于新出现的特性,要么确认你的用户使用现代浏览器,要么通过快速特性检查保护新代码,并保留回退方案:
if (typeof Intl.DurationFormat === "function") {
// 使用平台特性
} else {
// 回退到库或更简单的格式
}这样你可以向能够运行新特性的用户发送更少的代码,同时不破坏无法运行的用户。
总结
将这些聚类相加,画面变得具体。国际化相关功能 gzip 后约 14 KB,HTTP 相关约 17 KB,UI 原语约 24 KB,Lodash 工具库则根据你实际使用量在 8 KB 或更高。对于一个典型的中型应用,这意味着你可以将约 60 KB 到 90 KB 的依赖项交还给平台进行处理,如果你在使用完整版 Lodash 或同时使用多个库,这个数字会更高。(未压缩的数字是 gzip 前的 2 到 3 倍,这正是你在 bundle 分析器中看到的数值。)
我为这些功能选择了相对精简的包,但某些单个包仍可能很重。例如,你的 dialog 包可能单独就达到 50KB gzip 后的大小,具体取决于你使用的内容。
未来一年内有几个特性值得关注,因为它们将开启更多替换机会:
- Temporal 原生支持。一旦 Safari 在稳定版本中发布它并达到 Baseline 状态,你就可以同时移除日期库和 polyfill,将目前的倒退变为真正的胜利。
- CSS 锚定位功能逐渐成熟。该功能于 2026 年 1 月进入基准可用阶段。随着其逐步过渡到广泛可用阶段,为大众用户移除提示框和弹出框定位库的风险也逐渐降低。
- Object.groupBy 及相关方法进入广泛可用阶段。2024 年批次的功能(数组分组、Set 方法)预计将于 2026 年底实现广泛可用,这标志着它们从"需检查受众兼容性"转变为"可直接使用"。
这并非一次性清理工作。平台持续推出新功能,"需要依赖库实现"与"浏览器原生支持"之间的差距持续缩小。值得养成的习惯很简单:每季度运行一次审计。列出你的依赖项,检查哪些已进入基准可用阶段,将能替代的库逐步移除。
从本文中选择一个功能模块,打开你的 package.json,看看浏览器已经为你实现了多少。
(yk)
更多内容请访问
Smashing Newsletter 每周精选前端与用户体验干货,直达你的邮箱。真正实用的技巧与洞察。
前端与用户体验在线工作坊 实用技巧、直播课程、视频回放和互动问答,随时随地学习。
TypeScript 50 讲 全面解析 TypeScript,包含代码示例与实践教程。及其他纸质书籍。