我把祖传项目的构建时间砍了90%,领导以为我只是在"优化了一下",结果隔壁组的CI都崩了来问我配置

本文不是教程,是一次"地下重构"的完整复盘。


事情是这样的

公司有个三年前的 React 项目,Webpack 4 + Babel 7 + 一堆自定义 loader,构建时间稳定在 4分30秒 左右。

每次改个文案,等热更新要喝杯水。提个 PR,CI 跑完能去趟厕所再回来。

上周我实在是忍不了了。周五下午需求评审完,我 Jira 上只建了一张票:"优化构建配置",估点 2sp

领导看了一眼:嗯,小优化,去吧。

他不知道的是,我周末在家干了什么。


周六:先搞清楚这 4 分半都在干嘛

bash

ini 复制代码
# 先上正经 profiling
DEBUG=webpack* npm run build 2> webpack.log

然后写了个脚本分析:

JavaScript

javascript 复制代码
// analyze-build.js
const fs = require('fs');
const log = fs.readFileSync('webpack.log', 'utf8');

// 提取每个 loader 的耗时
const loaderTimes = log.match(/(\d+)ms.*?loader/g) || [];
console.table(loaderTimes.map(s => {
  const [time, name] = s.match(/(\d+)ms.*?(\w+)-loader/).slice(1);
  return { loader: name, time: +time };
}).sort((a, b) => b.time - a.time));

结果让我沉默了:

表格

Loader 耗时 备注
babel-loader 127s 全量转译,包括 node_modules
ts-loader 89s 类型检查跟编译捆在一起
sass-loader 45s 没开 fiber,同步解析
eslint-loader 38s 构建时全量 lint,包括第三方库

babel-loader 在转译 node_modules 里的 lodash。

我盯着这个结果看了三分钟。


周六晚上:第一刀,先砍最明显的

1. 把 babel-loader 的 include 收紧

JavaScript

javascript 复制代码
// 之前:没有 include,全量过 babel
{
  test: /.(js|jsx)$/,
  use: ['babel-loader']  // 127s
}

// 之后:只转译 src + 必要的 esm 包
{
  test: /.(js|jsx)$/,
  include: [
    path.resolve(__dirname, 'src'),
    // 只转译那些发布的是 esm 且需要兼容的包
    path.resolve(__dirname, 'node_modules/@company'),
    path.resolve(__dirname, 'node_modules/xxx-esm-pkg')
  ],
  use: ['babel-loader']
}

这一刀下去:127s → 34s。

但还不够。ts-loader 的 89s 也很离谱。


2. 把类型检查从构建流程里拆出来

JavaScript

javascript 复制代码
// 之前:ts-loader 自己做类型检查,阻塞编译
{
  test: /.tsx?$/,
  use: [
    'babel-loader',  // 转义
    {
      loader: 'ts-loader',
      options: { transpileOnly: false } // 默认就是 false!
    }
  ]
}

// 之后:ts-loader 只负责转译,类型检查交给 fork-ts-checker
{
  test: /.tsx?$/,
  use: ['babel-loader', 'ts-loader'] // ts-loader 默认 transpileOnly: false?
  // 不对,ts-loader 默认确实是 false,但我们可以显式优化
}

实际上我换了思路:

JavaScript

yaml 复制代码
// webpack.config.js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  module: {
    rules: [{
      test: /.tsx?$/,
      use: [
        'babel-loader',
        { loader: 'ts-loader', options: { transpileOnly: true } }
      ]
    }]
  },
  plugins: [
    new ForkTsCheckerWebpackPlugin({
      typescript: { diagnosticOptions: { semantic: true, syntactic: true } }
    })
  ]
};

类型检查移到子进程,不阻塞主构建流程。

这一刀:89s → 21s(ts-loader 部分),且热更新不再等类型检查。


3. sass-loader 开 fiber + 持久化缓存

JavaScript

javascript 复制代码
{
  test: /.scss$/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'sass-loader',
      options: {
        implementation: require('sass'),
        sassOptions: { fiber: false } // sass 1.33+ 废弃了 fiber,但...
        // 实际上我升级了 sass-loader 并启用了 webpack5 的持久化缓存
      }
    }
  ]
}

这里我直接上了 Webpack 5 的持久化缓存

JavaScript

lua 复制代码
// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

第二次构建:4分30秒 → 38秒。

冷启动还有优化空间,但热缓存已经起飞了。


周日:我动了邪念

构建快了,但我看着 webpack.config.js 里那坨三年前的配置,手痒了。

这项目当年是从 CRA eject 出来的,配置里还有 eslint-loader(CRA 3 时代的产物)、url-loaderfile-loader 混用、optimize-css-assets-webpack-plugincssnano...

我打开文档看了一眼:Vite 5 已经稳定了。

但迁 Vite 风险太大,我不敢。而且 Jira 票上写的是"优化构建配置",不是"重构构建工具"。

所以我折中了一下:用 Rspack 做一次"无痛迁移"。

Rspack 是字节出的,Webpack API 兼容,但用 Rust 重写,号称构建速度 5-10 倍。

我心想:反正都是 Webpack 配置,试试又不亏。

bash

sql 复制代码
npm install @rspack/core @rspack/cli --save-dev

然后把 webpack.config.js 改成 rspack.config.js,API 基本不用动:

JavaScript

css 复制代码
// rspack.config.js
const rspack = require('@rspack/core');

module.exports = {
  // 90% 的配置直接复制过来就能跑
  module: {
    rules: [
      // babel-loader 换成 @rspack/plugin-react
      {
        test: /.(js|jsx|ts|tsx)$/,
        use: {
          loader: 'builtin:swc-loader', // Rspack 内置 SWC,比 babel 快得多
          options: {
            jsc: {
              parser: { syntax: 'typescript', tsx: true },
              transform: { react: { runtime: 'automatic' } }
            }
          }
        }
      }
    ]
  },
  plugins: [
    new rspack.HtmlRspackPlugin({ template: './public/index.html' }),
    // 其他插件大部分都能直接用
  ]
};

跑了一下:

bash

复制代码
npx rspack build

冷构建:28秒。

有缓存:8秒。

我反复确认了三遍,没报错,产物正常,Source Map 也在。


周一:我撒了个小谎

晨会上,领导问:"构建优化那件事做得怎么样了?"

我说:"嗯,改了一些 loader 配置,构建快了一点。"

领导:"好,继续推进需求吧。"

我点点头,把 rspack.config.js commit 上去,PR 标题写的是:

chore: optimize build config and enable persistent cache

代码里确实也有 Webpack 5 缓存的配置(我保留了双份配置,Rspack 是主配置,Webpack 5 的作为 fallback),不算完全撒谎。


周三:事情开始失控

PR merge 的第二天,我收到一条飞书消息:

隔壁组后端 @我:"你们组 CI 怎么跑这么快?我们前端项目构建要 6 分钟,能参考下你们的配置吗?"

我还没回,又一条:

B 业务线前端负责人 :"听说你们构建优化了?能把 rspack.config.js 发我看看吗?"

然后架构群开始有人 @我:

"xxx 你们那个 Rspack 迁移有文档吗?我们组也想试试。"

我慌了。

因为我 根本没有写迁移文档 。那个 rspack.config.js 是我周末边试边改的,里面还有几行我自己都忘了干嘛的注释:

JavaScript

arduino 复制代码
// TODO: 这个 plugin 好像不加也行?先留着
// FIXME: 生产环境这里可能有问题,周末再测

而且最尴尬的是:我们的 CI 配置还是用的 webpack 命令,Rspack 是我本地开发用的。

也就是说,CI 上跑的还是优化后的 Webpack 5(28秒左右),本地开发用的是 Rspack(8秒)。

但隔壁组以为我全链路都切了。


周四:被迫写"文档"

下午被拉进了一个临时会议,标题是"《前端构建优化经验分享》"。

参会人:3 个业务线的前端负责人 + 架构组 + 我的领导。

领导看着我:"你优化得不错啊,给大家讲讲?"

我:"..."

然后我把周末干的事全抖了:

  1. Webpack 5 持久化缓存 + loader 范围收紧
  2. ts-loader transpileOnly + fork-ts-checker
  3. 本地开发切 Rspack(CI 还是 Webpack 5)

架构组的人问:"CI 为什么不一起切 Rspack?"

我说:"Rspack 的 copy-webpack-plugin 替代品有个 bug,生产环境我还没测完..."

其实是我周末没时间测了。

领导听完,沉默了一下,说:"那你这周把 CI 也切了吧,写个迁移文档,给其他组参考。"

我:"...好的。"


现在的情况

  • 我们组:本地 Rspack(8s),CI Rspack(15s),已稳定运行两周
  • A 业务线:抄了我的配置,但遇到 SWC 和 babel-plugin-import 的兼容问题,我来修的
  • B 业务线:直接上 Vite 了,说"反正都要迁,不如一步到位"
  • C 业务线:还在 Webpack 5,但用我的优化方案,构建从 5 分钟降到 40 秒

我的 Jira 票还是 2sp,状态"已完成"。

但我的飞书签名改成了: "构建优化咨询请先发红包。"


一些正经的总结

表格

优化手段 收益 风险
Webpack 5 持久化缓存 二次构建 10x 提升 缓存失效策略要配好
loader include 收紧 减少 60%+ 无效编译 要确认哪些包需要转译
ts-loader + fork-ts-checker 编译和类型检查并行 类型错误不会阻塞构建,需配 CI 检查
Rspack/SWC 冷构建 5-10x 提升 部分 babel plugin 不兼容,需逐个验证

最后

重构这件事,有时候不需要"立项评审"、"技术方案评审"、"排期"。

你只需要:

  • 一个忍不了的痛点
  • 一个不用加班的周末
  • 一张只写"优化一下"的 Jira 票

然后,等同事来找你要代码。


你有过类似的"地下重构"经历吗?评论区聊聊。


如果本文对你有帮助,欢迎点赞收藏。如果构建优化遇到问题,可以留言,我看到了会回(但不一定及时,因为可能在帮隔壁组修 CI)。

相关推荐
CoderWeen1 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
森鹿1 小时前
express中间件原理以及大致实现
前端·express
光影少年1 小时前
HashRouter 和 BrowserRouter 区别、底层原理、部署差异
前端·react.js·nestjs
风骏时光牛马1 小时前
JSP页面直接输出实体对象空属性引发页面500报错实战案例
前端
IT_陈寒2 小时前
Python里这个赋值坑,连老司机都能翻车
前端·人工智能·后端
Hyyy3 小时前
什么是bun?和pnpm有什么区别
前端·面试·bun
IT_陈寒16 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
kyriewen16 小时前
我用 50 行代码重写了 React Router 核心,终于搞懂了前端路由原理
前端·javascript·react.js
WebInfra17 小时前
Rspack 2.1 发布:React Compiler 提速 10 倍!
前端