Webpack打包速度優(yōu)化實踐

隨著項目的增大,webpack的打包速度已成前端工程師的“不可承受之重”。最近對團隊內某項目的打包速度進行了一些優(yōu)化,本文沒有具體的配置教程,只提供一些優(yōu)化思路,供啟發(fā)和參考。

更換更快的打包工具

1. bundler:代表webpack、parcel

parcel和webpack主要區(qū)別

  • parcel采用了類似于webpack中thread-loader的方式進行并行構建
  • parcel內建了類似于dll的緩存策略(webpack5中也內置了緩存策略)
  • parcel的HTML、JS 和 CSS 分別是通過 posthtml、babel 和 postcss處理的

更新webpack版本

webpack5內置了持久性緩存和跟好的緩存策略,Tree Shaking性能提升,搖掉更多無用代碼。
嘗試改善與網(wǎng)絡平臺的兼容性。

2. noBundler:代表snowpack、vite

主流的瀏覽器版本都支持直接使用 JavaScript Module。
HTTP/2可以合并請求來優(yōu)化模塊并發(fā)請求性能。
vite針對復雜的第三方庫,會自動識別并提前打包緩存起來,避免過多http請求(類似于dll)

從webpack遷移到vite
  • 項目里使用了create-react-app,內置了很多配置項(遷移成本高)

  • 團隊內部構建的SDK庫由于打包后的語法不標準,webpack不報錯,vite會報錯(更改SDK)


    css import語句只能在頂層
  • 項目里使用到了monaco-editor,官方提供的插件只有webpack版本,遷移到vite不好處理(團隊內部寫了rollup插件)

  • vite里less沒有autoimport的配置(寫死在less inject里)

  • vite變量形式和webpack不一樣,webpack可以識別process.env.xxx,vite是import.meta.xxx(想的是開發(fā)用vite,build用webpack,需要兼容)

  • 項目里用到的scoped-css-loader等loader沒有vite版插件(類似于@vitejs/plugin-vue-jsx,寫一個插件在vite里使用babel)

  • webpack可以很好的處理web和node共用變量(webpack可以轉換cjs,vite需要通過optimize項配置)

web和node共用變量

由于webpack到vite的遷移成本比較高,vite build時的速度和webpack也差不多,使用hardsource等插件后webpack dev的速度也是可以接受的,決定還是在webpack體系下做優(yōu)化

升級webpack

截止目前,cra正式版還停留在webpack4版本,好在alpha版升級到了webpack5,可以使用react-app-rewired start --scripts-version react-scripts來指定react-scripts版本。實測升級到alpha版后速度更慢了,在優(yōu)化了配置后速度也沒有明顯提升。
猜測原因是webpack5的緩存策略主要是dev的時候有用,本質上和hardsource沒有太大區(qū)別,但首次生成緩存的速度比hardsource稍快,生產環(huán)境中一般會禁用或重新構建緩存。

esbuild官網(wǎng)上也可見webpack5比webpack4的速度更慢了

替換babel
  • babel負責將js、ts、jsx等格式代碼轉換為js。相似功能的還有:tsc、esbuild、swc。

  • tsc在轉換時默認會檢查ts類型,插件環(huán)境沒有babel好,一般都會使用babel。

  • babel不會檢測類型,只負責把ts轉換到js,速度會比tsc要快,插件生態(tài)支持的也好一些(react熱更新、vue3jsx語法轉換官方都只提供了babel的插件)注:swc與nextjs合作,內建了react熱更新支持。esbuild調研后未發(fā)現(xiàn)支持,vite的react熱更新插件還是使用了babel

  • esbuild、swc也不會檢測類型,他們會用go和rust生成的二進制文件處理js或ts,速度比babel更快。如果沒有使用babel插件,可以在webpack中直接用esbuild-loader或swc-loader替換babel,但插件生態(tài)幾乎為零(減少了暴露給插件的API以提升速度,esbuild transformer無法使用插件)ps:Vercel團隊最近吸納了swc的作者,并在新版nextjs里提供了替換babel的選項,未來可期

  • 用babel、esbuild和swc時,需要使用fork-ts-checker-webpack-plugin校驗ts類型(cra默認啟用)

  • 一般在使用tsc打包的時候,也會關閉類型檢查并使用fork-ts-checker-webpack-plugin檢測類型以優(yōu)化速度。

  • fork-ts-checker-webpack-plugin是一個webpack插件,它會在打包時fork出一個進程并行進行檢查,可以更好的利用多核cpu的能力,過程中幾乎不影響webpack主進程,故可以優(yōu)化速度。

替換terser

terser負責壓縮babel和webpack生成的產物,去掉無效代碼,去掉日志輸出代碼,縮短變量名,生成source-map等,可以有效壓縮體積

  • terser是js寫的,壓縮時的內存占用、cpu占用都很高,雖然有cache、多進程等選項,但提升并不理想

  • esbuild帶有壓縮的功能,使用esbuild替換terser做壓縮,可以帶來比較大的速度提升,但生成產物比terser壓縮的大10%,在中后臺項目且會拆分文件的場景下,文件尺寸不是痛點。

  • 目前團隊有測試和預上線環(huán)境,目前測試和沙箱環(huán)境也會壓縮代碼,可以在測試和沙箱構建時禁用壓縮,經測試可以帶來40%左右的速度提升

source-map

webpack提供了如下的source-map選項,
不同選項的構建速度和性能以及適用場景都有很大差別,在這里不詳細敘述


source-map的速度以及適用場景比較
  • 團隊測試和沙箱環(huán)境構建時可以不分離sourcemap(或者用cheap-eval-source-map映射到行)以提升速度。

  • 用替代品生成sourcemap(用wasm-webpack-sources替換webpack-sources)


    wasm-webpack-sources
  • 更新webpack-sources的版本,ps: nextjs團隊在博客中提到,升級webpack-sources版本后,構建source-map的速度比不開啟僅多花了11%左右的時間


    nextjs博客
  • 延遲構建source-map,由于線上環(huán)境不會暴露source-map,可以先關閉source-map構建出一份產物到線上,然后打開source-map再在后臺打包一次并將source-map上傳到Sentry等監(jiān)控平臺和線上文件映射上。

使用多進程打包

happypack和thread-loader


image.png
  • happypack已經很多年沒有維護了,核心原理是啟用多進程,多個loader并行處理文件,happypack開發(fā)人員建議,如果使用webpack4及以上更推薦使用thread-loader,thread-loader做的事情和happypack一樣
  • thread-loader是webpack官方推薦的,原理是將loader放在單獨的一個worker進程內處理,但實測下來babel-loader前放置thread-loader后的速度更慢了


    無thread-loader

    有thread-loader

    開啟thread-loader時,監(jiān)測到有一瞬間fork出了很多node進程,但接下來就消失了。可能是因為以下限制(用了babel-plugin),實際thread-loader并沒有啟用成功


    thread-loader的限制
  • 在看到thread-loader官方的用例后,推測也可能是由于項目是ts的,webpack需要遞歸的調用babel-loader來解析語法和生成依賴關系,進程間通信會消耗大量時間


    thread-loader官方測試用例
使用緩存

cache-loader、開啟loader自帶的cache選項、dll、hardsource、webpack5

  • cache-loader也是webpack官方推薦使用的,加在耗時較長的loader前面,在heavy loader執(zhí)行前,對比要處理的文件和緩存文件的mtime,mtime沒變的話直接取緩存文件,實測構建時間會變長。
    loader: ['cache-loader', 'other-loader']
    簡單看了一下,cache-loader分兩個階段:pitch和loader
    pitch階段的處理流程是:cache-loader -> other-loader
    loader階段的處理流程是:other-loader -> cache-loader

pitch階段根據(jù)當前正在處理的文件,讀取.cache目錄中對應的cache文件,對比mtime判斷是否可以復用
loader階段依賴pitch階段的判斷,如果pitch階段判斷當前文件的緩存失效了,loader階段就要重新生成緩存。

  • babel-loader自帶了cache選項,但babel-loader的cache必須經過一次編譯,才會將索引的文件與文件編譯結果緩存在內存中。在后續(xù)的編譯過程中,如果發(fā)現(xiàn)索引的文件已經緩存過了,才會直接引用已經編譯緩存的結果。(還是會有編譯的過程)
  • dll動態(tài)鏈接方案


    DLL和緩存的區(qū)別

    可以將共用不經常改變的依賴(如react、react-dom、vue、antd、moment)打包成dll
    webpack打包引入庫時入口會被動態(tài)指向dll文件里,實測是有用的,但dll方案在18年左右被社區(qū)的腳手架拋棄,大概意思是使用dll會大量增加維護的成本,(我在使用時也遇到有些插件打成dll后報錯),webpack4相比webpack3帶來的打包速度提升使得dll有些得不償失


    cra

    vue-cli

hardsource和webpack5
hardsource和webpack5持久化緩存的方案類似,webpack5持久化緩存結果至硬盤上,第一次編譯文件的時候,計算文件的hash。將編譯結果與hash關聯(lián)起來。第二次編譯文件時,首先加載本地緩存結果,進入正常編譯環(huán)節(jié)時,對編譯的文件再次求hash,如果此hash在緩存庫中已經存在了,那么將直接跳過編譯環(huán)節(jié),直接輸出編譯結果。
這兩種方案都是dev的時候才會有用(記得官方有個issue說實驗基于緩存build可能會有5%的概率出錯),升級到cra5之后發(fā)現(xiàn)復用緩存的條件極其嚴格,每次重新build時都會重新構建緩存,hardsource首次構建的消耗時間比較大,webpack5由于的緩存是基于webpack4構建時的內存改造得來,首次構建帶來的額外時間消耗并不大,二次構建hardsource和webpack5的速度相當。

external
  • external之后會把webpack會把import語法轉成訪問全局變量,直接忽略語法解析,也不會調用loader。
    external的主要問題是,有些庫之間相互依賴,比如antd依賴moment和react,mobx-react-lite依賴react,也依賴了mobx,引入順序和cdn質量需要額外花精力維護,很多庫官方也沒有提供umd的包,使用第三方的umd包可能會有問題。
  • react中比較穩(wěn)定的umd庫有react react-dom antd moment,可以放心external掉

external可以配合Systemjs使用,systemjs支持直接引入esm而不必去找umd,webpack4也內建支持把libraryTarget改成SystemJs的形式,直接使用esm格式包時不需要type=module,也可以進行動態(tài)加載改造,微前端框架single-spa也使用這種方式進行依賴動態(tài)加載

<script type="systemjs-importmap">
        {
               imports": {
                   "react": "http://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js",
                   "react-dom": "http://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js",
                   "moment": "http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.0/moment.min.js",
                   "antd": "http://unpkg.com/antd@4.6.6/dist/antd.min.js",
                   "handsontable": "http://cdn.jsdelivr.net/npm/handsontable@8.4.0/dist/handsontable.full.min.js",
                   "braft-editor": "http://cdn.jsdelivr.net/npm/braft-editor@2.3.9/dist/index.min.js",
                   "lodash": "http://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
                   "mobx": "http://cdnjs.cloudflare.com/ajax/libs/mobx/5.15.7/mobx.min.js",
                   "webpackBundle":"./output.bundle.js"
               }
           }
       </script>
<script>
System.import('xxx').then((res) => {
   System.import('webpackBundle');
 })
</scirpt>
webpack5 Module Federation預編譯node_modules

module federation 是 webpack5 提出的新特性,含義為模塊聯(lián)邦。主要是使用于微前端場景,可以在運行時動態(tài)引入子應用。
優(yōu)化思路:利用 webpack5 的 module federation 特性,構建一個虛擬的 federation 應用,項目直接僅使用編譯好的依賴,這樣就可以直接減去熱更新和啟動時對依賴的重新編譯。實際和dll的原理類似?但速度比dll要快很多

module federation

umi使用module federation優(yōu)化的思路

業(yè)內腳手架umi已在這方面做出了實踐,但沒有提供通用的webpack插件,可以期待未來社區(qū)的產出

硬件更換

由于JS的多線程能力不佳,webpack在打包時更吃CPU單核性能,多核性能幾乎(在多核服務器上測試還沒有筆記本塊)沒有用。
截止目前,蘋果還沒有發(fā)布第二代自研桌面處理器,(APPLE M1x在制程不變的情況下無法大幅提高主頻,大概率會通過堆核心、增加GPU、增加總線帶寬、提高內存頻率等方式做優(yōu)化,可以預見單核性能不會有類似Intel -> M1的巨大提升)基于的硬件選購建議是,M1 16g配置的Mac電腦在未來幾年內都會是非常適合前端開發(fā)者使用的(傳統(tǒng)x86芯片的電腦在不改變封裝邏輯的前提下預計相同價位提升至M1的單核性能水平需要很長時間,基于JS的打包生態(tài)遷移到rust和go也需要很久)。

使用nice命令提高本機webpack進程優(yōu)先級后速度也略有提升(需要root權限)

總結

基于上述調研和項目的業(yè)務場景,最終決定應用以下優(yōu)化

  1. 用esbuild替換terser作壓縮
  2. 測試和預上線環(huán)境禁用壓縮并啟用cheap-source-map
  3. 應用dll
    以下是在本機的一些實驗結果


    external和dll

    esbuild壓縮測試

    優(yōu)化后vs優(yōu)化前生產環(huán)境構建提升了大約43%的時間,測試環(huán)境構建提升了大約58%的時間


    項目測試

一些困惑

  1. Module Federation為什么比dll快
  2. 能不能使用JS實現(xiàn)多線程打包

參考

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容