隨著項目的逐步迭代,代碼量和依賴的逐漸增長,應用的構建速度逐步進入緩慢期。以目前所在團隊的業務應用來看(使用webpack構建),應用整體構建耗時已經普遍偏高,影響日常開發測試的使用效率,其中編譯耗時大約占50%。
實際上隨著近些年前端的技術發展以及業務對前端交互體驗的要求提高,前端整個代碼量復雜度和代碼量增長飛快。隨著這一趨勢的變化,服務于前端工程構建方案多年的webpack,在構建效率上已經逐漸成為瓶頸。因此業界也存在不少優化思路和方案,主要分兩個方向:
既然是業界的普遍性問題,那么外界也肯定會存在不少優化案例可以借鑒或者復用。由于Node.js的優化方案通常都會存在各種場景限制,這里我們主要從另外一個思路去尋找方案。經過調研目前業界的主要方案有Rspack、Vite、Turbopack、swcpack相對比較有代表性(可能還有其他方案,由于筆者時間精力有限未能了解到)。幾個方案主要的情況如下(筆者個人主觀分析,僅供參考):
圖片
先看當前大倉前端應用主要技術體系:整體技術棧主要是React為主,未來Vue還會逐步遷移React;框架層面UmiJS@4大概占60%、UmiJS@v3大概占20%、剩余為其他Vue或者C端的多頁應用。整體技術體系主要是UmiJS為主,配套主要是webpack的構建方案,部分Vue項目有使用Vite。
在這樣的現實情況下,我們面對的主要是React+UmiJS+webpack的應用。基于應用廣泛性考慮,只要解決這部分應用就可以達到80%的應用提速覆蓋。為此,我們選擇了基于Rspack來實現構建方案的性能提升,主要考慮有以下幾點:
圖片
綜合情況來看:雖然性能Rspack未必是最高的,但其兼容webpack生態帶來的低成本遷移,是其他的方案基本上無法做到的。對此,選擇了基于Rspack來作為基礎的底層能力。得益于此,我們最終實現了業務代碼零改動即可實現構建方案遷移(僅微調構建配置),并獲得云構建2倍+編譯性能的提升。
由于大倉目前大量的應用為UmiJS@4體系,作為我們主要服務的目標對象,本文也是主要先針對解決UmiJS@4版本的應用方案。
UmiJS默認構建能力是封裝在@umi/max內部,通過max dev/build來調用內置配套的本地/生產構建。在UmiJS官方上并未提供編譯能力的完全自定義擴展能力,僅支持Vite/webpack的選擇以及提供了一些修改構建配置的方法。
通過查看UmiJS項目的源碼,發現其內部的構建實現全部集中在dev/build這兩個擴展命令中。源代碼在preset-umi/src/commands/目錄下的dev/dev.ts和build.ts 兩個文件,分別對應dev和build命令。而實際上的構建邏輯,實現上主要是在 @umijs/bundler-vite 和 @umijs/bundler-webpack兩個包中。其內部執行的主要邏輯如下(這里以dev模式為例,build模式基本上類似,就是少了文件監聽編譯和express相關邏輯):
圖片
那么我們只需要在新的命令中實現相關的邏輯,即可通過擴展命令的方式來擴展Rspack構建能力。另外,使用該方法實現,還能沿用UmiJS原本的代碼生成等原本的插件,進而可以避免需要大量重新實現UmiJS插件的能力,并降低重寫帶來的邏輯不一致風險。
前面的插件擴展模式已經能保證原有大量的UmiJS能力是可以直接沿用的,比如生成路由、添加tailwind.css等UmiJS內置能力都可直接沿用。另外項目主要依賴擴展的插件,除了少部分修改構建能力的插件之外,基本上都可以直接使用或者少量適配即可。不兼容的幾個插件主要是因為:功能是通過修改的webpack配置來調整構建能力,需要通過使用對應的Rspack構建能力來進行兼容。
另外除了UmiJS的插件之外,還有構建依賴的Babel插件部分,當然部分Babel插件也是通過UmiJS插件引入使用的。對此,也對主要的Babel插件的情況進行了梳理,其主要的情況如下表:
圖片
所以整體上還是以沿用webpack原有擴展加Rspack官方能力替換為主,只需要針對少部分未支持的Babel插件進行擴展即可實現。
想要達成方案可以快速應用的理想效果,就是讓應用接入過程中盡可能少改內容,特別是業務代碼。因為一旦要改業務代碼邏輯,這種就會增加非常多的接入成本,所以在方案設計上,構建能力以及原本UmiJS的相關配置能力要盡量去沿用并且滿足基本上不需要業務側同學去感知差異化的內容。
帶著這個需求并結合前面的幾塊內容分析來看,大多數內容都是有平替方案,少部分需要進行自定義開發擴展,主要也是集中在Babel插件上。那么我們需要做的就是維持UmiJS轉換生成webpack的配置邏輯不動,在拿到webpack配置之后,再對webpack的配置做解析通過一個配置轉換器對需要轉換成Rspack的內容進行轉化,對原本兼容的配置直接遷移使用即可實現我們的目的。
配置&能力映射示意圖:
圖片
結合以上的問題解決思路以及目標,最終方案的架構設計如下:
架構要點說明:
切換構建之后,less/postcss插件是一致的,主要風險來自于兩個方面:
圖片
圖片
雖然在Rspack方面申明已經兼容了主流的內容,但畢竟是替換了構建方案,對業務來說還是存在一些未知的風險,還是需要一些手段來進行保障業務應用的穩定性。
穩定性保障手段:
實現超低接入成本:僅需改動三個小步驟,一兩分鐘即可完成接入。具體步驟如下:
dx add @umijs/plugin-rspack@latest -D
{ // 原有其他配置 ... plugins: [ // 原有其他插件 ..., // 添加 @umijs/plugin-rspack 插件 '@umijs/plugin-rspack', ], // 原有其他配置 ...}
{ "scripts": { // start 對應支持本地的dx dev, // 原配置樣例 - "start": "cross-env BUILD_ENV=dev max dev", // 改rspack構建樣例 + "start": "cross-env BUILD_ENV=dev NODE_ENV=development max rspack-dev", // pnpm:build:x 對應支持發布平臺指定的環境 // 原t1配置樣例 - "pnpm:build:t1": "cross-env BUILD_ENV=t1 max build", // t1改rspack構建樣例 + "pnpm:build:t1": "cross-env BUILD_ENV=t1 NODE_ENV=production max rspack-build", ... // 原先的其他配置,酌情進行調整 }}
平均2倍+的編譯性能提升:大倉應用接入17個應用(目前主要是接入開發、測試環境),平均提升在2倍以上。以自身負責的一個應用為例,原有webpack編譯耗時150秒,接入后降低到40秒(減少73.33%),加上優化過程中去除部分無用的引入代碼最終僅需20秒左右。
這里主要結合UmiJS所需要的能力,分享一些UmiJS涉及到的Rspack用法以及過程中一些比較典型的內容。
ts/tsx的支持主要依靠的是swc-loader,Rspack使用了Rust定制的builtin:swc-loader,其使用方式基本上是跟webpack的swc-loader一致的,詳情可以直接參考swc-loader文檔,這里主要體現一些常用的配置項,具體使用可以結合自身項目情況來調整。
export default { module: { rules: [ { test: //.(j|t)s(x)?$/, loader: 'builtin:swc-loader', options: { env: { // 瀏覽器兼容性,支持browserslist,詳細可以參考:https://swc.rs/docs/configuration/supported-browsers#targets targets: { chrome: "80", }, }, // js/ts編譯配置 jsc: { parser: { // 開啟ts編譯 syntax: 'typescript', // 開啟tsx編譯 tsx: true, // 開啟@裝飾器編譯 decorators: true, // 動態import // dynamicImport: false, }, transform: { // react運行環境配置 react: { // dev模式打開development啟動react的開發模式 development: isDev, }, // stage 1 的舊版本class decorators legacyDecorator: true, // 支持 ts emitDecoratorMetadata decoratorMetadata: true, }, }, }, }, ] }}
在看文檔配置時,感覺不好理解,實際使用上其實分兩種情況:
import ReactRefreshPlugin from '@rspack/plugin-react-refresh';export default { ... 其他配置 devServer: { // 開啟HMR,官方文檔也有體現 hot: true, }, plugins: [ ...其他插件 // React熱更新支持插件 isDev && new ReactRefreshPlugin(), ]}
Rspack提供了兩個版本的模塊聯邦能力,官方文檔主要是介紹的v1.5對應的是經過Rspack改良過的版本。但實際上在一些情況下,如果直接跟webpack的MF對接會存在一些問題。我們在第一次使用過程中,也是對接了默認的v1.5版本,就出現了公共依賴無法找到的問題,錯誤提示如下:
圖片
實際上若需要跟webpack項目對接的情況下,需要采用v1.0版本的MF插件,使用方法如下:
import { container } from '@rspack/core';export default { ... 其他配置 plugins: [ new container.ModuleFederationPluginV1({ ... 這里是mf的配置 }) ]}
樣式按需引入:babel-plugin-import
babel-plugin-import作用:主要是配合ant-design或poizon-design(基于antd定制主題)庫來使用,通過插件識別js中依賴的組件,自動注入樣式文件,進而實現css的按需引用。實現類似如下效果:
// 手寫源代碼import { Button, Input } from 'antd';// 通過插件編譯后import { Button, Input } from 'antd';import 'antd/lib/button/style';import 'antd/lib/input/style';
在Rspack中使用的是swc來進行js/ts解析的,故babel-plugin-import是不能直接使用的,需要采用builtin:swc-loader的rspackExperiments來進行對應的能力支持。其配置方法如下:
export default { module: { rules: [ { test: //.(j|t)s(x)?$/, loader: 'builtin:swc-loader', options: { experimental: { // babel-plugin-import的配置 import: [ // pd按需注入樣式 { libraryName: 'poizon-design', libraryDirectory: 'es', style: true }, // antd按需注入樣式 { libraryName: 'antd', libraryDirectory: 'es', style: true }, ], } } } ] }}
cssModules:auto-css-modules
在UmiJS中,默認是通過auto-css-modules來進行css-modules代碼的判斷;其原理為通過ast語法樹,判斷import引入樣式文件時,若有聲明對應的變量,就將其打上一個query標記。并在樣式文件處理時,通過resourceQuery來進行匹配對應的文件,并添加css-modules的編譯能力。詳情見:UmiJS AutoCssModules插件源代碼、UmiJS CSS編譯配置源代碼。
通過UmiJS項目以及結合一些資料,Babel的auto-css-modules能力有swc-plugin-auto-css-modules可以替代,配置的話,主要幾個關鍵點:
具體配置實現參考如下:
const postcssLoader = { loader: require.resolve('postcss-loader'), options: { // ...此處省略less-loader配置 }};const lessLoader = { loader: require.resolve('less-loader'), options: { // ...此處省略less-loader配置 }}export default { builtins: { css: { // cssModule默認配置 modules: { // class保持原樣輸出 localsConvention: 'asIs', // class轉換后的格式,localIdentName跟css-loader并不完全兼容,比如[hash:base64:5]這種寫法就會報錯 localIdentName: '[local]_[hash:8]', }, }, }, module: { rules: [ { test: //.(j|t)s(x)?$/, loader: 'builtin:swc-loader', options: { experimental: { plugins: [ // ...此處其他配置內容 // 添加 swc-plugin-auto-css-modules 插件執行?modules的注入 [require.resolve('swc-plugin-auto-css-modules'), {}] ] } }, }, { test: //.css(/?.*)?$/, oneOf: [ { // 通過resourceQuery來匹配?modules resourceQuery: /modules/, use: [postcssLoader], // type聲明指定為cssModules解析 type: 'css/module' }, { use: [postcssLoader], // type聲明指定為普通css解析 type: 'css' }, ] }, { test: //.less(/?.*)?$/, oneOf: [ { resourceQuery: /modules/, use: [postcssLoader, lessLoader], type: 'css/module' }, { use: [postcssLoader, lessLoader], type: 'css' }, ] }, ] }}
但實際上上述的配置并不能直接跑起來,運行時會提示構建報錯(RuntimeError: out of bounds memory accesSS),錯誤如下圖:
圖片
結合github上的相關issue最終定位為swc_core版本不兼容的問題導致。
原因詳解:
問題解決辦法:實際上只要通過將swc-plugin-auto-css-modules的swc_core版本修改為0.88.x~0.89.x這個范圍內,再編譯出新的wasm文件即可解決;若有其他類似情況,也可以借鑒一下。
具體操作步驟:
plugins: [ // ...此處其他配置內容 // 添加 swc-plugin-auto-css-modules 插件執行?modules的注入 // 刪掉原有的 swc-plugin-auto-css-modules 引入 - [require.resolve('swc-plugin-auto-css-modules'), {}] // 修改為引入項目相對目錄下的swc_plugin_auto_css_modules.wasm(因為開源的發版有發版周期,先進行自編譯使用) + [path.join(__dirname, '../../swc-plugins/swc_plugin_auto_css_modules.wasm'), {}],]
影響編譯效率的devtool: "source-map"
在以往使用webpack的devtool時,也能感受到有一定的性能開銷;同時在Rspack中,官方文檔中也有說明當devtool開啟并設置sourcemap時有性能開銷。官方文檔:
圖片
實際體驗下來,source-map的模式損耗確實是大,大約會增加30%的耗時,而其他一些模式在構建損耗上會有所優化,具體可視使用情況來選擇建議在開發測試環境不使用,僅在線上等需要的環境開啟。相同應用和機器環境,不同devtool的幾種測試表現如下:
devtool: source-map(40.82s)
devtool: cheap-module-source-map(39.46s)
devtool: eval(32.94s)
devtool: false (31.21s)
在前面的內容中有提到babel-plugin-import按需引入的利器,但在業務項目中也有發現,應用/組件中有直接引入.../dist/antd.less的情況。若在項目中有這種情況,實際上就是打包了多次antd的樣式,非但沒有作用反而讓構建打包和應用訪問性能更差。
以下為英特爾i5芯片差異情況(M2等芯片在耗時上會縮小一些):
在css-moduels中(比如:大多數頁面less、組件less):每引入一次,大約增加6~7s左右構建時間,css文件大約增加660kb。
在非css-moduels中(比如:global.less):每引入一次,大約增加4~5s構建時間,css文件大約增加530kb。
本文鏈接:http://www.tebozhan.com/showinfo-26-100464-0.html基于Rspack實現大倉應用構建提效實踐 聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com 上一篇: 如何在 Flutter 中實現 2D 可滾動的表格,探索 Flutter 中的二維可滾動項,表格解決方案 下一篇: 消息隊列中間件詳解,你學會了嗎?