转译配置实战:解决项目中常见的构建难题
优化前:卡得不行
项目上线前做性能测试,首页加载直接 5 秒起步,热更新编译要等快 10 秒,开发体验烂到不想碰。最离谱的是,每次改一行代码,HMR 要转好久才生效,浏览器都快刷新出感情了。同事说我这不是开发环境,是冥想环境——等编译就是在修行。
生产构建更别提,CI/CD 流水线经常超时,打包时间稳定在 40s 左右,部署频率直接被劝退。这种速度别说优化用户体验了,连开发者自己都用不下去。
一开始我以为是组件太重、图片太多,结果一通操作下来发现,真正的问题出在转译配置上。我用的是 Babel + Webpack,默认配了一堆 preset 和 plugin,看着挺标准,实则全是冗余转译。
找到病根了!
先上 Chrome DevTools 的 Performance 面板录了一段 HMR 更新过程,发现主线程有大量 babel.transformSync 调用,持续占用 CPU。然后切到 Webpack Bundle Analyzer 看产物,发现很多 ES6+ 语法明明现代浏览器都支持了,还被转成了 var、polyfill 塞进去。
比如一个简单的箭头函数:
const greet = () => 'hello'
硬生生被转成:
var greet = function greet() {
return 'hello';
};
这谁受得了?而且每个文件都来一遍,积少成多,编译时间直接爆炸。
我还试了 esbuild-loader 和 swc-loader 替换 babel-loader,确实快了些,但兼容性问题一堆,某些 plugin 不支持,改完跑不起来,折腾半天又回退了。
最后意识到:不是工具不行,是我配置太糙了。不该转的全转了,该缓存的没缓存,target 写了个 browserslist:production 就完事,根本没细调。
动刀:精准转译 + 缓存策略
核心思路就两个:按需转译、充分利用缓存。
首先明确目标运行环境。我们业务只支持现代浏览器(Chrome >= 80, Safari >= 14),那完全没必要为 IE 兼容买单。于是我把 Babel 的 targets 改得更激进:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "80",
"safari": "14"
},
"modules": false,
"useBuiltIns": "usage",
"corejs": { "version": 3, "proposals": true }
}
]
]
}
关键点在 "modules": false —— 让 Webpack 自己处理模块系统,避免 Babel 多一次转换。同时 useBuiltIns: "usage" 按需引入 polyfill,而不是全量打进去。
然后是缓存。之前 .babelrc 改了没加缓存配置,loader 每次都重新跑。加上 cacheDirectory 和 cacheCompression:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false // 开发环境关掉压缩,提升读取速度
}
}
}
]
}
};
这里注意我踩过好几次坑:cacheCompression 默认是 true,会把缓存文件 gzip 压缩,听起来省空间,但读写反而慢,尤其在 SSD 上差别明显。关掉之后,热更新编译时间立竿见影降了 1.2s 左右。
再往下,我发现 node_modules 里的包其实大部分已经是编译过的,没必要全走 babel-loader。于是加了个更细粒度的排除规则:
{
test: /.js$/,
use: 'babel-loader',
include: [
path.resolve(__dirname, 'src'),
// 只对特定依赖转译,比如用了 ES6+ 但没编译发布的库
/node_modules[/\]some-library/
]
}
有些第三方库用了可选链、空值合并这些新语法,但发布时没转译,浏览器直接报错。这种就得单独拎出来处理,不能一刀切 exclude node_modules。
顺手优化:Polyfill 别乱塞
之前 @babel/polyfill 直接 import 一次,结果 Array.from、Promise 这些全被打包进去,哪怕我只用了一个 includes。
改成 useBuiltIns: "usage" 后,Babel 会根据源码实际使用的 API 自动注入最小化 polyfill。比如我用了 Object.entries(),它只引入对应的 core-js 模块,而不是整个标准库。
有个小坑:如果你用了动态 import() 或 top-level await,记得装 regenerator-runtime 并手动注册:
import 'regenerator-runtime/runtime';
不然 async 函数运行时报 regeneratorRuntime is not defined,这个错误我在本地没问题、线上报错,查了半小时才发现 CI 环境 tree-shaking 把 runtime 干掉了。
优化后:流畅多了
改完之后,开发环境热更新从平均 9.8s 降到 2.1s,HMR 基本秒响应。生产构建也从 42s 降到 26s,减幅接近 40%。
更重要的是包体积小了。首屏 JS 从 1.2MB 降到 890KB,Gzip 后从 320KB 降到 240KB,加载时间从 5.1s 降到 1.8s(3G 网络模拟)。
数据对比如下:
- 开发构建时间:9.8s → 2.1s(↓78.6%)
- 生产构建时间:42s → 26s(↓38.1%)
- 首包体积(gzip):320KB → 240KB(↓25%)
- 首屏加载时间:5.1s → 1.8s(↓64.7%)
说实话,没做到极致,但已经够用。比如还可以引入 SWC 做部分转译,或者用 esbuild 打包,但我评估了一下迁移成本和团队维护难度,决定先稳住当前方案。
还有哪些可以搞
其实还有几个方向能继续压:
- 用
@swc/jest替换babel-jest,测试启动更快 - 把 Babel 配置拆成 development / production 两套,开发时进一步减少检查
- 尝试 Vite,彻底换掉 Webpack,不过涉及改造太大,暂时搁置
目前这套配置已经跑在项目里一个多月,没出过大问题。唯一的小问题是某些低版本安卓 WebView 仍不支持 optional chaining,但我们业务不覆盖那部分用户,所以无视了。
以上是我的优化经验,有更优的实现方式欢迎评论区交流
这个技巧的拓展用法还有很多,后续会继续分享这类实战踩坑文。如果你也在被转译慢折磨,不妨检查下自己的 babel-loader 配置是不是太“保守”了。
很多时候,不是工具不行,是我们给它干了太多不该干的活。

暂无评论