本文不是教學,是一次「地下重構」的完整復盤。
公司有個三年前的 React 專案,Webpack 4 + Babel 7 + 一堆自訂 loader,建置時間穩定在 4 分 30 秒 左右。
每次改個文案,等熱更新要喝杯水。提個 PR,CI 跑完都能去一趟廁所再回來。
上週我實在是忍不了了。週五下午需求評審完,我 Jira 上只建了一張票:「優化建置設定」,估點 2sp。
主管看了一眼:嗯,小優化,去吧。
他不知道的是,我週末在家幹了什麼。
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-loader127s全量轉譯,包括 node_modules``ts-loader89s型別檢查跟編譯綁在一起sass-loader45s沒開 fiber,同步解析eslint-loader38s建置時全量 lint,包括第三方庫babel-loader 在轉譯 node_modules 裡的 lodash。
我盯著這個結果看了三分鐘。
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 也很離譜。
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 部分),且熱更新不再等型別檢查。
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-loader、file-loader 混用、optimize-css-assets-webpack-plugin 配 cssnano...
我打開文件看了一眼: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 個業務線的前端負責人 + 架構組 + 我的主管。
主管看著我:「你優化得不錯啊,給大家講講?」
我:"..."
然後我把週末幹的事全抖了:
transpileOnly + fork-ts-checker架構組的人問:「CI 為什麼不一起切 Rspack?」
我說:「Rspack 的 copy-webpack-plugin 替代品有個 bug,生產環境我還沒測完...」
其實是我週末沒時間測了。
主管聽完,沉默了一下,說:「那你這週把 CI 也切了吧,寫個遷移文件,給其他組參考。」
我:"...好的。"
我的 Jira 票還是 2sp,狀態「已完成」。
但我的飛書簽名改成了: 「建置優化諮詢請先發紅包。」
表格
優化手段收益風險Webpack 5 持久化快取二次建置 10x 提升快取失效策略要配好loader include 收緊減少 60%+ 無效編譯要確認哪些套件需要轉譯ts-loader + fork-ts-checker編譯和型別檢查並行型別錯誤不會阻塞建置,需配 CI 檢查Rspack/SWC冷建置 5-10x 提升部分 babel plugin 不相容,需逐個驗證---
重構這件事,有時候不需要「立項評審」、「技術方案評審」、「排期」。
你只需要:
然後,等同事來找你要程式碼。
你有過類似的「地下重構」經歷嗎?留言區聊聊。
如果本文對你有幫助,歡迎按讚收藏。如果建置優化遇到問題,可以留言,我看到了會回(但不一定及時,因為可能在幫隔壁組修 CI)。