转译配置实战:解决项目中常见的构建难题

Newb.圆圆 工具 阅读 1,788
赞 28 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线前做性能测试,首页加载直接 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.fromPromise 这些全被打包进去,哪怕我只用了一个 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 配置是不是太“保守”了。

很多时候,不是工具不行,是我们给它干了太多不该干的活。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论