core-js entry-based 与 usage-based 引入策略的原理与最佳实践

作者:资源驿站

时间:2026-11-06

浏览:511

详细讲解 core-js 两种引入策略 entry-based 和 usage-based 的工作原理、配置方法、产物差异与选择依据,帮助开发者根据项目特点做出正确选择。

文章目录

在使用 Babel 编译现代 JavaScript 时,@babel/preset-env 的 useBuiltIns 选项决定了 core-js polyfill 的引入方式。它有两个实际生效的值:"entry"(entry-based)和 "usage"(usage-based)。二者的区别不在于"能否 polyfill",而在于哪些 polyfill 被放进产物、由谁来决定。理解它们各自的工作流程和产物差异,是控制打包体积、保证兼容性的关键。

一、两种策略的核心区别

一句话概括:entry-based 根据你声明的目标浏览器,把所有这些浏览器可能缺失的 polyfill 全部替换进来;usage-based 根据你代码里实际用到的 API,只引入对应的 polyfill。

它们的入口、触发条件和产物体积都不同:

  • entry-based:需要你在入口文件手动写 import "core-js/stable",Babel 会把这一条 import 拆成目标浏览器缺失的所有模块。
  • usage-based:不需要手动 import,Babel 分析每个文件的 AST,发现用到了 Promise、Array.from 等 API 时自动注入对应模块。
core-js entry-based 与 usage-based 引入策略的原理与最佳实践
图1:entry-based 与 usage-based 的完整工作流程对比

二、entry-based 策略详解

配置方法

在 babel.config.js 中,将 useBuiltIns 设为 "entry",并指定 core-js 版本:

// babel.config.js
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "entry",
        corejs: "3.33",  // 指定项目安装的 core-js 版本
        targets: "> 0.25%, not dead"
      }
    ]
  ]
};

然后在项目入口文件(通常是 src/index.js 或 main.js)顶部添加:

import "core-js/stable";
import "regenerator-runtime/runtime";

Babel 的处理过程

编译时 Babel 会把 import "core-js/stable" 这一条语句展开,替换成一系列细粒度的模块导入。这些模块由 targets 配置决定——如果你的目标浏览器包含 IE 11,展开后可能有几百条 import;如果只需支持最近两个版本的 Chrome,展开后只有几十条。

替换后的代码类似这样:

// Babel 编译产物(示例,实际条目由 targets 决定)
import "core-js/modules/es.promise.js";
import "core-js/modules/es.promise.finally.js";
import "core-js/modules/es.array.from.js";
import "core-js/modules/es.array.flat.js";
import "core-js/modules/es.array.flat-map.js";
import "core-js/modules/es.object.entries.js";
import "core-js/modules/es.string.pad-start.js";
// ... 可能还有上百条

关键特征:无论你的代码里用没用 Array.prototype.flat,只要目标浏览器缺少这个 API,它的 polyfill 就会被引入。策略是"宁可多,不可漏"。

适用场景

entry-based 适合以下情况:

  • 项目依赖的第三方库内部使用了现代 API,但没有自带 polyfill。entry-based 在入口把所有目标浏览器缺失的特性都补齐了,第三方库在运行时就能正常工作。
  • 需要确保全局环境的完整性,不想逐一排查哪些 API 被间接用到。
  • 对产物体积容忍度较高,或者项目本身已经很大,polyfill 的增量占比不明显。

三、usage-based 策略详解

配置方法

// babel.config.js
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage",
        corejs: "3.33",
        targets: "> 0.25%, not dead"
      }
    ]
  ]
};

使用 "usage" 时,不需要在入口文件手动 import core-js。如果你写了 import "core-js",反而会造成重复引入。

Babel 的处理过程

Babel 在编译每个文件时,遍历 AST(抽象语法树),识别代码中使用的特性。例如:

// 源代码:src/utils.js
const items = Array.from(document.querySelectorAll(".item"));
const result = items.flatMap(el => el.dataset.tags.split(","));
const p = new Promise(resolve => resolve(42));

Babel 检测到 Array.from、Array.prototype.flatMap、Promise 三个 API,结合 targets 判断目标浏览器是否支持,然后在文件顶部自动插入:

// Babel 编译产物
import "core-js/modules/es.array.from.js";
import "core-js/modules/es.array.flat-map.js";
import "core-js/modules/es.promise.js";

const items = Array.from(document.querySelectorAll(".item"));
const result = items.flatMap(el => el.dataset.tags.split(","));
const p = new Promise(resolve => resolve(42));

如果另一个文件 src/format.js 只用了 String.prototype.padStart,那这个文件的产物里只会出现 import "core-js/modules/es.string.pad-start.js",不会引入 Promise 或 Array.from 的 polyfill。打包工具(webpack / Rollup)最终会对所有 import 去重,同一个模块只打包一次。

检测的局限

usage-based 的精确性依赖于静态分析,而静态分析存在边界:

  • 动态属性访问无法识别。如果通过 obj[methodName]() 调用了需要 polyfill 的方法,Babel 无法从静态代码推断 methodName 的值,不会注入任何 polyfill。
  • 第三方库不被分析。Babel 通常不编译 node_modules 内的代码。如果一个第三方库内部使用了 Object.fromEntries,而你的代码从未用过这个 API,usage-based 不会为它引入 polyfill。运行时在老浏览器上就会报错。
  • 全局类型污染检测不完美。对于原型链上的方法(如 [1,2].includes(1)),Babel 需要推断 [1,2] 是数组才能正确注入 es.array.includes。大多数情况能识别,但嵌套链式调用有时会遗漏。
core-js entry-based 与 usage-based 引入策略的原理与最佳实践
图2:usage-based 策略能正确检测的调用方式与无法覆盖的盲区

四、产物体积的实际差异

为了直观理解两种策略的体积差异,用一个典型的中型 SPA 项目做对照。项目代码使用了 Promise、Array.from、Object.assign、String.prototype.includes 这几个需要 polyfill 的 API,目标浏览器是 "> 0.25%, not dead"(包含部分较旧的移动端浏览器)。

entry-based 编译后引入约 180 个 core-js 模块,polyfill 部分约 90KB(压缩后约 30KB)。usage-based 只引入上述 4 个 API 对应的模块及其依赖共约 15 个,polyfill 部分约 12KB(压缩后约 4KB)。差距在 6 到 7 倍。

当项目使用的现代 API 越少、目标浏览器越旧,差距越大。反过来,如果项目大量使用了各种新特性(如 WeakRef、Array.prototype.at、Object.hasOwn、structuredClone 等),usage-based 引入的模块数量会逐渐接近 entry-based。

core-js entry-based 与 usage-based 引入策略的原理与最佳实践
图3:相同目标浏览器下,entry-based 与 usage-based 的 polyfill 体积差异

五、配置中的关键细节

corejs 版本号要写精确

corejs 选项建议写成带次版本号的字符串(如 "3.33"),而不是整数 3。写整数 3 等同于 "3.0",Babel 只会引入 core-js 3.0 就有的 polyfill,后续版本新增的特性(比如 3.22 加入的 Array.prototype.at)不会被处理。

// ✗ 不推荐
corejs: 3           // 等同于 "3.0"

// ✓ 推荐
corejs: "3.33"      // 与 package.json 中安装的版本对齐

targets 的影响

targets 直接决定了引入多少 polyfill。在两种策略中都起作用:

  • entry-based 下,targets 越宽(支持的旧浏览器越多),展开的模块越多。
  • usage-based 下,即使代码用到了某个 API,如果 targets 中所有浏览器都原生支持,Babel 也不会注入 polyfill。

如果没有配置 targets,Babel 默认编译到 ES5,会引入几乎所有 polyfill——此时两种策略的体积差距反而不大,因为 usage-based 也需要补齐非常多的特性。

debug 选项查看引入详情

加上 debug: true 可以在编译时输出 Babel 实际引入了哪些 polyfill,帮你确认策略是否符合预期:

["@babel/preset-env", {
  useBuiltIns: "usage",
  corejs: "3.33",
  targets: "> 0.25%, not dead",
  debug: true
}]

编译时控制台会打印类似信息:

// 示例输出
[src/utils.js] Added following core-js polyfills:
  es.array.from { "ie":"11" }
  es.promise { "ie":"11", "samsung":"4" }
  es.promise.finally { "ie":"11", "samsung":"4" }

六、选择策略的判断依据

选择哪种策略,取决于三个条件的综合权衡:

第一,你的项目是否依赖了不经 Babel 编译的第三方库?如果是,usage-based 可能遗漏这些库内部用到的 API。此时 entry-based 更安全,因为它不看代码用了什么,而是把目标浏览器缺的都补上。

第二,你对产物体积的敏感程度。移动端 H5、需要极致首屏加载速度的场景,polyfill 体积差几十 KB 是可感知的。usage-based 的优势在这种场景下非常明显。

第三,项目是应用还是库?如果你在开发一个供他人使用的 npm 库,通常不应该在库的产物中包含 polyfill(无论哪种策略)。polyfill 的决定权应该留给最终的应用。库应该使用 @babel/plugin-transform-runtime 配合 corejs: 3 来处理,这会引入不污染全局的 helper,而不是修改全局原型。

注意:两种策略都会向全局注入 polyfill(修改 Array.prototype、window.Promise 等)。如果你的代码运行在不能污染全局的环境中(如微前端沙箱内),需要改用 @babel/plugin-transform-runtime 的 corejs 选项,它通过模块化导入避免全局修改。
core-js entry-based 与 usage-based 引入策略的原理与最佳实践
图4:根据项目类型和约束选择 polyfill 引入策略的决策流程

七、两种策略的组合与补救方案

在实际项目中,有时单一策略不能完美解决问题。以下是两种常见的补救方式:

usage-based + 手动补充。选择 usage-based 获得较小体积,同时在入口手动 import 已知被第三方库依赖但 Babel 检测不到的模块:

// src/index.js
// 手动补充:某个 UI 库内部使用了 Object.fromEntries
import "core-js/modules/es.object.from-entries.js";
// 其余 polyfill 由 Babel 的 usage-based 自动处理

这种方式的缺点是需要你知道第三方库用了哪些 API,通常要通过运行时报错或阅读库源码才能发现。

针对不同 targets 拆分产物。现代打包工具(如 webpack 5 + browserslist)支持构建多份产物:给现代浏览器一份几乎不含 polyfill 的 ES2017+ 产物,给旧浏览器一份包含完整 polyfill 的产物,通过 <script type="module"> 和 <script nomodule> 分别加载。这种方案独立于 entry/usage 的选择,但可以进一步缩小现代浏览器用户的下载体积。

八、总结

entry-based 是"声明式"的——告诉 Babel 你要支持哪些浏览器,它把缺失的 polyfill 全补上,覆盖全面但体积较大。usage-based 是"推断式"的——Babel 分析你的代码,只引入用到的 polyfill,体积小但可能遗漏编译范围外的依赖。

对大多数独立应用项目,如果没有复杂的第三方兼容性问题,usage-based 是更优的默认选择。如果你需要为大量第三方库兜底,或者项目对体积不敏感,entry-based 更省心。无论选哪种,都要确保 corejs 版本号写准确、targets 配置合理,并善用 debug: true 核实实际引入结果。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章