:全家桶移除 — Babel 全家桶深度分析)
Webpack迁到Vite第七节二全家桶移除 — Babel 全家桶深度分析本篇定位Babel 在 Webpack 时期扛着语法转译、API polyfill、helper 去重三座大山。迁移到 Vite 后 esbuild 接手了语法转译但 polyfill 和 helper 的问题在新架构中怎么解决本篇逐包拆解四个 Babel 依赖移除的原因以及 Vite 对应的替代链路。思维导图Babel 全家桶移除 ├── babel/core — 核心编译器 │ └── esbuild 替代目标浏览器原生 ES6无需语法降级 ├── babel/preset-env — 按 browserslist 转译 │ └── 替代esbuild target: modules不降级 plugin-legacyES5 兜底 ├── babel/plugin-transform-runtime — helper 去重 │ └── 替代chunk 级编译天然共享regeneratorRuntime 残留但不修 └── babel-plugin-import — antd 按需加载 └── 替代统一入口导入 Rollup tree-shaking生产 Vite 自动依赖预构建开发1.2 Babel 相关4 个—— 逐包深度分析本册分析 Babel 全家桶 4 个包整个第七节系列合计移除 16 个包七(一) 构建核心 4 本册 4 七(三) Loader/Plugin 6 七(三) 其他 3 个中移除 2 个core-js 保留升级另有后续 lodash 替换。Babel 相关4 个babel/core— Babel 核心原用途Babel 的核心编译器负责将 ES6 代码解析为 AST抽象语法树然后应用插件转换最后生成目标代码。Babel 的处理流程输入源码ES6 │ ▼ ① 解析Parser: 源码 → AST │ babel/parser 将字符串解析为 AST 树 │ ▼ ② 转换Transform: AST → AST │ 遍历 AST 节点应用 preset/plugin 的转换规则 │ babel/preset-env → 转换箭头函数、const/let 等 │ babel/plugin-transform-runtime → 复用辅助函数 │ ▼ ③ 生成Generator: AST → 输出代码 │ babel/generator 将 AST 转回字符串 │ ▼ 输出代码ES5为什么不再需要Babel 在这里做的核心工作是ES6 语法 → ES5 语法转译箭头函数→function、const/let→var、class→构造函数等以兼容browserslist中可能出现的旧浏览器 1%覆盖率可能包含 IE11 等不支持 ES6 的浏览器。Vite 时期vite.config.js未显式设置build.target。Vite 7 的默认值为baseline-widely-available约 Chrome 107/Firefox 104/Safari 16且本项目因使用vitejs/plugin-legacy未传modernTargetstarget 被覆盖为插件的默认现代浏览器集合[es2020, edge79, firefox67, chrome64, safari12]。无论哪一层这些目标浏览器原生支持 ES6 语法——const、箭头函数、class、async/await等都不需要转译。因此esbuild 不需要做同样的工作它根本就不需要做 ES6→ES5 的语法降级。// vite.config.js — 没有显式设置 build.target// Vite 7 默认 baseline-widely-available本项目经 plugin-legacy 覆盖为现代浏览器集合// esbuild 不转译任何 ES6 语法因为目标浏览器原生支持这不是esbuild 比 Babel 更快地完成了转译而是转译这个工作量本身被消除了——目标浏览器升级了不再需要照顾 ES5 兼容。ES5 兼容场景如签署人使用旧 Android 手机由vitejs/plugin-legacy处理——让 Babel 重回构建链路生成传统产物已实施详见 第七节五ES5 降级方案专题。babel/preset-env— 按目标环境转译原用途根据目标浏览器列表browserslist自动确定需要转译哪些 ES6 语法。例如如果目标浏览器支持箭头函数babel/preset-env就不会转换箭头函数如果不支持就转换为普通函数。babel/preset-env的工作逻辑// babel.config.jspresets:[[babel/preset-env,{useBuiltIns:usage,// 按需引入 polyfillcorejs:3,// 使用 core-js 3}],// 注意没有 targets 选项目标浏览器由 package.json 中的 browserslist 统一管理]// package.json{browserslist:[ 1%,// 全球使用率超过 1% 的浏览器last 2 versions,// 每个浏览器最近 2 个版本not ie 10// 排除 IE10 及以下]}为什么targets不在babel.config.js中browserslist是前端工具链的共享配置——不只 Babel 在用autoprefixerCSS 前缀、postcss-preset-envCSS 特性降级、eslint-plugin-compat兼容性检查等工具都会读取同一份browserslist。把它放在package.json的顶层所有工具自动共享无需在每个工具配置中重复声明。babel/preset-env在不传targets时默认从以下位置按优先级查找browserslistpackage.json的browserslist字段项目根目录的.browserslistrc文件BROWSERSLIST环境变量browserslist各条目含义条目含义 1%全球市场份额超过 1% 的浏览器版本last 2 versions每个浏览器的最新 2 个主版本如 Chrome 120、Chrome 119not ie 10排除 IE10 及以下它做了三件事查browserslist确定目标浏览器范围查babel/compat-data确定哪些语法需要转换只转换目标浏览器不支持的语法为什么不再需要babel/preset-env根据browserslist确定需要降级哪些语法如箭头函数→function、const→var。esbuild 也有target选项可以控制语法级别但在本项目配置中esbuild 的target为现代浏览器集合chrome64 等不做任何 ES6→ES5 转译——因为目标浏览器已原生支持 ES6 语法。babel/preset-env的语法降级工作不是被 esbuild 替代了而是因为目标浏览器升级而不再需要了。ES5 降级的场景交由vitejs/plugin-legacy内部使用 Babel处理而非 esbuild 的target已实施见第七节五。babel/plugin-transform-runtime— 辅助函数复用原用途Babel 在转换代码时会生成一些辅助函数helper比如_classCallCheck、_extends、_defineProperty等。默认情况下每个文件都会内联这些函数导致大量重复代码。没有babel/plugin-transform-runtime时// 文件A.jsfunction_classCallCheck(instance,Constructor){...}// 内联 helpervarAfunctionA(){_classCallCheck(this,A);}// 文件B.js同样的 helper 再内联一次function_classCallCheck(instance,Constructor){...}// 重复varBfunctionB(){_classCallCheck(this,B);}有babel/plugin-transform-runtime时// 从 babel/runtime 统一引用不重复内联var_classCallCheckrequire(babel/runtime/helpers/classCallCheck);varAfunctionA(){_classCallCheck(this,A);}为什么不再需要esbuild 没有辅助函数的概念——它直接生成目标代码不产生中间 helper。新增的 plugin-legacy 走先打包后转译路径chunk 级编译helper 在 chunk 内天然共享不需要babel/runtime外部包。transform-runtime 实际处理了哪些 helperWebpack 时期 dist 产物实测项目源码中的 Babel helper: _extends: 43 次class 继承/Object.assign 兜底 _typeof: 0 次源码无直接 typeof Symbol _slicedToArray: 0 次源码解构均可在目标环境原生运行 _asyncToGenerator: 0 次async/await 在旧 browserslist 目标中需降级但已由 regenerator 处理 外部文件pdf.worker.min.js中的 helper: 2,624 次预编译文件未经项目 Babel 处理项目源码中只有_extends产生了 43 次 helper全部由transform-runtime转为babel/runtime引用。其余 helper 类型_typeof、_slicedToArray等在项目源码中零出现——Vue 2 Options API 以对象字面量为主不使用class、极少解构深层嵌套。在 plugin-legacy 流程中Babel 处理的是 Rollup 输出的 chunk189 个每个 chunk 内 helper 定义一次后共享——不需要babel/runtime外部运行时。效果等价于transform-runtime但不引入额外依赖。⚠️ 深入分析plugin-legacy 下的 helper 与 regeneratorRuntime上文提到helper 在 chunk 内天然共享——这对_classCallCheck、_extends等短 helper 成立。但async/await的转译走的是另一条路径babel/preset-env使用babel/plugin-transform-regenerator将async/await转为 generator regeneratorRuntime后者是一段约1.9KB的 generator 运行时minified会被内联到每一个需要它的 chunk 中。实测数据npm run build产物全文搜索Helper 类型出现文件数来源_classCallCheck1pdf-legacyPDF.js 第三方_createClass1pdf-legacy_asyncToGenerator1pdf-legacyregeneratorRuntime105项目业务代码关键发现项目自身代码Vue 2 Options API不产生_classCallCheck/_asyncToGenerator等短 helper——这些 helper 全部来自第三方pdfjs-dist唯一大量出现的是regeneratorRuntime——189 个 legacy chunk 中 105 个使用了async/await重复开销计算单文件 regeneratorRuntime 大小: 1.9 KB (minified) 受影响文件数: 105 / 189 个 legacy 文件 原始总重复: 105 × 1.9 KB 198 KB gzip 后重复 (30% 压缩率): ~59 KB 项目总构建产物 (dist/): 约 22 MB 重复占比 (gzip): 0.3%以核心 mixinenvelopeMixin-legacy.js约 73KB为例regeneratorRuntime 占 2.7%。那为什么不加babel/plugin-transform-runtime来解决vitejs/plugin-legacy的 Babel 配置在源码中硬编码// node_modules/vitejs/plugin-legacy/dist/index.jsbabelrc:false,// 不读 .babelrcconfigFile:false,// 不读 babel.config.js且插件公开的 Options 接口中没有预留 Babel plugin 注入点——无法通过vite.config.js添加babel/plugin-transform-runtime。要绕过这个限制有以下三种方案但各有硬伤方案是什么怎么做硬伤patch-packagenpm 生态工具修改node_modules/源码后生成.patch文件Git diff提交到仓库。团队npm install后通过postinstall: patch-package自动应用补丁① 改vitejs/plugin-legacy/dist/index.js②npx patch-package vitejs/plugin-legacy③ 提交patches/目录依赖升级时 patch 大概率冲突需手动重做团队须理解 patch 的存在和原理wrapper plugin自定义 Vite 插件包裹在legacy()外面通过transform钩子拦截 legacy chunk在官方插件处理完后再跑一次 Babel二次注入babel/plugin-transform-runtime① 在vite.config.js中写一个{ name, transform(code,id){...} }插件② 放在legacy()之后注册③ 对含-legacy标识的 chunk 调 Babel与 plugin-legacy 内部实现强耦合chunk 命名、转换时机二次 Babel 增加构建时间fork 插件把vitejs/plugin-legacy源码复制到项目中修改后作为本地插件使用脱离 npm 官方版本① 拷贝源码到本地② 修改 Babel 配置③import本地路径完全失去官方更新安全补丁、bug 修复需手动追踪上游变更并合并无论哪种方案都是在和插件设计对抗。决策❌ 不加。优化收益 0.3%且仅影响旧浏览器实现成本高需对抗插件架构还需引入babel/runtime生产依赖。保持vitejs/plugin-legacy开箱配置即可。babel-plugin-import— antd 按需加载原用途babel-plugin-import自动把import { Button } from ant-design-vue转换为按需导入路径// 开发者写的代码import{Button}fromant-design-vue// babel-plugin-import 转换后的代码importButtonfromant-design-vue/lib/button// 只导入 Buttonimportant-design-vue/lib/button/style// 只导入 Button 样式但实际上本项目从未启用它。babel.config.js中babel-plugin-import的配置被注释掉了// babel.config.js实际代码// lazy load ant-design-vue// if your use import on Demand, Use this code// plugins.push([import, {// libraryName: ant-design-vue,// libraryDirectory: es,// style: true// }])module.exports{presets:[...],plugins:[babel/plugin-transform-runtime],// 注意只有 transform-runtime没有 import}项目采用的是手动子路径导入——开发者在src/components/pc/antd.js中手工书写了 111 个子路径57 个组件 53 个样式 1 个 version 导入// src/components/pc/antd.js实际代码import{defaultasButton}fromant-design-vue/es/buttonimportant-design-vue/es/button/styleimport{defaultasModal}fromant-design-vue/es/modalimportant-design-vue/es/modal/style// ... 40 个组件为什么用子路径导入通过实验验证在 Webpack 生产构建环境下用同一个 ant-design-vue 1.7.8 做了对照实验import { Button } from ant-design-vue主入口导入import Button from ant-design-vue/es/button子路径导入构建时遍历的模块数46634最终 bundle 大小566,067 bytes566,066 bytes结论Webpack 的 tree-shaking 能正确处理 antd 的 JS 代码——两个 bundle 大小几乎相同差 1 byte。但主入口导入时 Webpack 需要遍历解析全部 466 个模块包括未使用的 Affix、Anchor、Carousel 等构建速度明显慢于子路径的 34 个模块。子路径导入的真正价值不是减小产物体积tree-shaking 已经做到了而是按需加载样式每个组件的样式.less文件标记在sideEffects中es/**/style/*无法被 tree-shaking。使用主入口import { Button } from ant-design-vue不会自动引入 Button 的样式——需要额外import ant-design-vue/es/button/style。子路径导入 手动样式行让开发者显式控制每个组件的样式加载减少构建时解析的模块数34 vs 466Webpack 只需解析实际使用的模块树构建更快babel-plugin-import 的作用自动化上述手动过程——开发者写import { Button } from ant-design-vue插件自动拆分为组件子路径 样式子路径省去手动维护百余行导入的繁琐为什么不再需要Vite 使用 Rollup 做生产构建Rollup 的 tree-shaking 同样能正确处理 antd。更重要的是迁移到 Vite 后采用了import ant-design-vue/dist/antd.less全量样式导入替代 53 个独立样式导入配合 Vite 开发模式的自动依赖预构建两端都得到了更好的方案Webpack 时期babel-plugin-import未启用→ 手动子路径导入 → 组件按需 样式按需 Vite 时期 统一入口导入 → Rollup tree-shaking 全量样式文件 开发模式Vite 自动扫描并预构建依赖 → 减少请求数 生产构建Rollup tree-shaking 自动处理遗留代码清理src/components/pc/antd.js和src/components/h5/vant/index.js中的大量子路径导入antd 111 条 vant 54 条是项目手写的与babel-plugin-import无关。在 Vite 开发模式每个子路径是一次独立 HTTP 请求改为统一入口导入后import { Button } from ant-design-vue首屏请求数从 110 降到 ~4。详见第九节。optimizeDeps的角色澄清optimizeDeps.include是 Vite 自动依赖扫描的兜底机制——本项目所有依赖antd、vant、moment、lodash-es均在main.js中直接 import自动扫描已全部覆盖该字段已从vite.config.js中删除完整演进见第六节 §1.9。开发与生产的模块处理分工开发模式Vite 自动扫描入口 import → esbuild 预构建 → 合并碎片模块 → 减少 HTTP 请求 生产构建Rollup tree-shaking → 静态分析 ESM import/export → 移除未使用代码 → 减小 bundle 体积Loader/Plugin6 个内容见第七节三附录Babel vs esbuild 深入对比为什么 esbuild 比 Babel 快维度Babelesbuild实现语言JavaScriptGo解析器babel/parserJS 实现的 AST 解析器自研 Go 解析器转换方式AST → 插件遍历 → 修改 AST → 生成代码AST → 直接生成目标代码并行能力单线程需 worker 池手动并行原生多线程Go 的 goroutine启动时间~500ms加载 JS 模块~10ms原生二进制进程缓存策略需手动配置cacheDirectory内置缓存插件生态大量可定制插件有限设计上故意保持简单速度对比数据来自 esbuild 官方基准测试Babel 转换 100KB 代码~500msesbuild 转换 100KB 代码~50ms速度差距10x ~ 20x为什么 Go 比 JS 快两个核心原因编译为机器码Go 编译为原生二进制直接在 CPU 上运行。Babel 是 JavaScript需要在 Node.js 的 V8 引擎中解释执行即使有 JIT 编译仍有解释器开销多线程Go 的 goroutine 允许在多个 CPU 核心上并行处理文件。JavaScript 的单线程模型需要手动创建 worker 线程池才能利用多核esbuild 能覆盖 Babel 的哪些功能功能Babelesbuild本项目覆盖情况ES6 语法转译箭头函数/const/class 等babel/preset-envtarget为现代浏览器集合 → 不做转译不需要目标浏览器原生支持按需 polyfilluseBuiltIns: usage不支持不需要目标浏览器原生支持 APIJSX/TSX 转译需要插件内置支持—本项目无 JSX/TSX 源码自定义插件如babel-plugin-import丰富插件生态不支持❌ 用统一入口导入 Rollup tree-shaking 替代辅助函数复用babel/plugin-transform-runtime无 helper 概念不适用不做转译不产生 helperesbuild 的 target 选项如何工作// vite.config.js — 本项目没有显式设置 build.target// Vite 7 默认 baseline-widely-available本项目经 plugin-legacy 覆盖为 [es2020,edge79,firefox67,chrome64,safari12]// 含义目标浏览器支持原生 ESM → 不转译任何 ES6 语法实际 target 下的行为esbuild不做任何语法降级。const保持const箭头函数保持箭头函数class保持classasync/await保持原样。因为目标浏览器chrome64/firefox67/safari12 等全部原生支持这些语法。如果显式设置target: es2015本项目未设置仅作参考esbuild 会进行以下转换target的含义是输出代码需兼容的最低浏览器版本。target: es2015意味着目标浏览器支持 ES2015——因此 ES2015 引入的语法const/let、class、箭头函数等无需转换但 ES2015之后引入的语法async/await、??等需要转换【转换】ES2015 之后引入的语法目标浏览器不支持 async/awaitES2017 → generator regenerator 或 Promise 链 ??ES2020 空值合并 → 显式 null/undefined 检查 ?.ES2020 可选链 → 显式属性存在检查 【保留】ES2015 及之前已存在的语法目标浏览器原生支持 const/letES2015 → 保留就是 ES6 箭头函数ES2015 → 保留 classES2015 → 保留 template literalES2015 → 保留 destructuringES2015 → 保留 【始终保留】ES5 及更早的语法不涉及转换 var、function、for、if 等注意esbuild 只转换语法不转换API。Object.entries()是 API不是语法。如果目标浏览器不支持Object.entriesesbuild 不会自动 polyfill。需要自己通过core-js引入。ES3 / ES5 / ES2015 的区别这三个级别经常被混淆但它们之间有明确的语法边界ES3 (1999) ES5 (2009) ES6 / ES2015 │ │ │ ▼ ▼ ▼ 已有: var 新增: strict mode 新增: const / let function JSON.parse/stringify 箭头函数 for / while Array.forEach/map class 原始对象操作 Object.create 模板字符串 ${} getter / setter Promise use strict Symbol / Map / Set for...of / 解构 ────────────────────────────────────────────────────────────────────────────── 典型浏览器: 典型浏览器: 典型浏览器: IE6 ~ IE8 IE9 ~ IE11 Chrome 49 Safari 9 Firefox 52ES5 ≠ ES2015ES2015即 ES6在 ES5 基础上新增了const/let、箭头、class、模板字符串等语法两者相差 6 年标准演进。IE 11 是 ES5 级别浏览器——它不支持任何 ES2015 语法。对本项目的意义Webpack 时期的 browserslist 包含 IE 11Babel 必须以 ES5 为目标const→var、箭头→function、class→构造函数。Vite plugin-legacy 使用相同的 browserslist降级目标同样是 ES5。两者都不是 ES2015、也不是 ES3——ES3 早已被 ES5 全面覆盖不需要单独考虑。esbuild 的 target 支持范围实测验证es3 ❌ 不存在此选项 es5 ❌ ERROR: Transforming const to the configured target environment (es5) is not supported yet es2015 ✅ es2016 ✅ ... es2022 ✅ esnext ✅esbuild 的 target最低只到es2015不支持es5或更低。这意味着如果目标浏览器不支持 ES2015如 IE11、旧 Android WebViewesbuild无法将const/let/class/箭头函数等语法转为 ES5ES5 降级必须走vitejs/plugin-legacy内部用 Babelesbuild 在架构层面就放弃了 ES5vitejs/plugin-legacy不是可选的优化是ES5 兼容的唯一可行路径本项目不需要 polyfill 的推理vite.config.js中build.target未显式设置——Vite 7 默认为baseline-widely-available本项目经vitejs/plugin-legacy覆盖为现代浏览器集合chrome64/firefox67/safari12等。这个范围包括Chrome 642018Firefox 672019Safari 122018Edge 792020这些浏览器已经实现了 ES2020 及以上的大部分 API。项目中使用的 ES6 APIPromise、Object.assign、Array.includes、Map、Set等在这些浏览器中都原生支持不需要 polyfill。本节小结Babel 四个包全部移除不是因为 esbuild更快地做了同样的事而是因为转译工作量本身被消除了——目标浏览器chrome64 等现代集合已原生支持 ES6。唯一遗留的是 plugin-legacy 下的regeneratorRuntime跨 chunk 重复105/189 个 chunk但优化收益不足 0.3% 且实现路径被锁死决策不加。