roadhog 構建優化


背景

一個 antd 項目打包時間太長,竟然快二十分鍾了,有時還會導致內存溢出,查了一些資料(thanks funfish),解決方法如下

roadhog.js問題

roadhog.js 是類似可配置的 react-create-app,只是這個可配置,也只是部分可配置的,木有辦法,只能從源碼開始看 webpack 配置。

在進行 npm run build 的時候發現終端有提示 Creating an optimized production build... 的字樣,而且出現的時間也挺晚的,以前其他項目上面從未見過,難道是 roadhog 自己的?這個時候 webpack 居然還沒有開始構建?抱着疑惑,從 roadhog 的bin/roadhog.js就開始打印當前時間,再在到開始webpack構建的時候再打印一次時間。

結果這個過程要花上2931ms,還是可以接受的,只是明明第一次的時候記得等了很久的,為什么這次只要3s不到?后面又試了幾次,耗時均3s左右,后來想起了Webpack 構建性能優化探索里面提到的初次構建和再次構建的問題,一般再次構建耗時都要比初次構建的要少。會不會第一次比較慢是初次構建,后面都是再次構建呢?初次構建和再次構建有什么區別?百度和谷歌都沒有查詢到答案,只有該博客提到比較多。為了再現問題,well,重啟電腦,再次 npm run build 不就是初次構建嗎?結果還正如此。

優化webpack

按照Webpack 構建性能優化探索里面給出的思路,對於webpack的優化,可以從四個維度考量:

  • 從環境着手,提升下載依賴速度;
  • 從項目自身着手,代碼組織是否合理,依賴使用是否合理,反面提升效率;
  • 從 webpack 自身優化手段着手,優化配置,提升 webpack 效率;
  • 從 webpack 可能存在的不足着手,優化不足,進一步提升效率。

從環境出發這一點,是因為不同的nodejs版本和npm版本,有着顯著的性能差異來的。可以這么認為最新版本的nodejs/npm自然有更優秀的性能。由於項目本身用的就是最新版本的環境,所以這里也不加以分析了。

從項目中出發

首先用比較常規的方法,通過 webpack-bundle-analyzer 來查看 webpack 體積過大問題,結果如下圖所示:

圖挺好看的,乍一看沒有什么特別的地方,好像每個打包文件都是由諸多細文件組成的。並從文件大小來看壓縮過后都在1M以下,無可厚非。但是細心對比下,還是有不少發現。

案例1:為了實現小功能而引用大型 lib

這里用 webpack-bundle-analyzer 來查看打包過大問題,但是在引用的時候,卻發現roadhog原本自身就用了 webpack-visualizer-plugin 插件,只是在analyze指令下才能進入分析,整理之后webpack配置如下:

1 var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
2 var Visualizer = require('webpack-visualizer-plugin');
3 
4 plugins: [
5   new Visualizer({
6     filename: './statistics.html'
7   })],
8   new BundleAnalyzerPlugin()
9 ]

 

這里 webpack-bundle-analyzer 可以給予直觀的整體感受,而 webpack-visualizer-plugin則細化到每個文件中,每個模塊的百分比。

首先看到的是:最大的文件打包壓縮后是815.9kb,相對於其他較大的文件大出了整整400kb,這里肯定是有什么問題,細看之后,發現用了支付寶的 G2,在代碼中的體現是:

import G2 from 'g2';
// 將整個G2都引入進來了,導致文件過大

 

遺憾的是目前G2沒有實現按需加載的功能,在issue里面也只是表示正在討論而已(慶幸這里只是用了G2,沒有用到Data-set)。

仔細看了每個js文件打包構造后,發現有個文件也用了 moment 模塊,在印象中是基本沒有用到的。moment 模塊大小為53.2kb,而在總的打包文件中占 131.6kb。正同 Webpack 構建性能優化探索所說的,如果不想簡單實現,就采用 fecha 庫來代替 moment,fecha 要比 moment 小很多。只是替換后,發現 moment 體積並沒有降低多少,由於出處是在 index.js 文件里面,可能的地方只有 dva 了。只是 dva 怎么可能用到moment?完全不可能的,他的package.json里面也同樣沒有用到。通過排除法最后定位到如下:

1 // @src/index.js
2 import 'moment/locale/zh-cn';
3 
4 // @src/router.js
5 import { LocaleProvider } from 'antd';

 

第一個行代碼是直接使用了 moment 模塊,該代碼看着作用不大,而且查閱Ant-Design-Pro的歷史版本,均沒有發現在index.js里面使用 moment/locale/zh-cn。細心觀察,發現在 index.js里面使用了 moment/locale/zh-cn 之后,其他幾處用到moment的地方,生成文件都沒有明顯 moment 包,這些文件的體積基本上要減少一個 moment 的大小。這個moment/locale/zh-cn,還能降低其他文件體積。
第二行代碼,是在 router.js 文件里面,由於使用了 LocaleProvider 組件,這個組件通過源碼可以發現直接引用了 moment 模塊 import * as moment from 'moment'。當然同樣也起到了 moment/locale/zh-cn 的效果,能降低其他原本含 moment 文件的體積。

案例2:廢棄依賴沒有及時刪除

項目中用的是 Ant Design,import 的時候,組件是按需加載的,並不會整個引入 Ant Design,但是由於敏捷開發周期較短,新建頁面不會從零開始寫,基本都是移植相似的頁面,由此導致了Ant Design組件的亂引用。

由於 G2 的不可按需加載,以及 moment 在 Ant-Design 中的作用,工程的打包體積和打包時間沒有較大的減少。

從 webpack 自身優化點出發

webpack 本身也提供了許多優化的插件,但是由於經常接觸不多,許久后容易遺漏,導致再次學習的成本高。一個好的腳手架,是相當重要的。

webpack 自帶的優化

webpack 就有不少內置的插件。

  • CommonsChunkPlugin

CommonsChunkPlugin 可以從 module 提取公共 chunk,實現降低模塊大小,有利於整體工程打包后的瘦身。
CommonsChunkPlugin 這個插件在Vue-cli中也有用到,如下:

 1 // split vendor js into its own file
 2 new webpack.optimize.CommonsChunkPlugin({
 3   name: 'vendor',
 4   minChunks: function (module, count) {
 5     // any required modules inside node_modules are extracted to vendor
 6     return (
 7       module.resource &&
 8       /\.js$/.test(module.resource) &&
 9       module.resource.indexOf(
10         path.join(__dirname, '../node_modules')
11       ) === 0
12     )
13   }
14 }),
15 new webpack.optimize.CommonsChunkPlugin({
16   name: 'manifest',
17   chunks: ['vendor']
18 })

 

把相同的 chunk 提取出來,命名 vendor 與 manifest,前者是常說的公共 chunk 部分,后者是由於代碼變動導致 chunk 的 hash 值變化,導致公共部分在每次打包時都會有不一樣的 hash 值,使得客戶端無法緩存 vendor。**由於代碼變動導致 hash 變化,而生成的代碼,自然而然的會落在最后配置的 commonschunk 上面,**所以這部分可以單獨提取,命名為 manifest。

在roadhog里面,剛開始看以后沒有CommonsChunkPlugin的配置,想着趕緊提個issue,但是后面發現,是通過common.js引入,只有在roadhog里面配置了multipage選項為true的時候,才執行CommonsChunkPlugin插件。其代碼如下:

1 var name = config.hash ? 'common.[hash]' : 'common';
2 ret.push(new _webpack2.default.optimize.CommonsChunkPlugin({
3   name: 'common',
4   filename: name + '.js'
5 }));

 

通過 CommonsChunkPlugin 插件,node.js 在打包的時候,峰值內存增加了40M,就是約5.4%的內存,打包時間延長了大約6s,而構建后項目體積基本不變,what?有點震驚。只有負面效果。。。。看構建文件,只提取了一個公共文件,大小1kb,而且內容為一句普通的錯誤打印。為什么人與人之間沒有相互的chunk可以提取呢?

通過反復查 roadhog/ant-design/ant-design-pro 的 issue 都沒有類似的問題,似乎用了 babel-plugin-antd 對 antd 進行按需加載,沒有辦法將其提取到 vendor 里面了。如若不想不想按需加載,直接用 cdn 不就好了。但是現在想的是只要單獨的提取antd里面幾個涉及CRUD的重要組件:表格,form,日歷這幾個組件能否實現單獨打包到vendor?難道是我打開方式不對嗎?大神里面少 7s,我還多了 6s。。。。

在這個issue里面看到了這種寫法,頓時覺得沒錯,就是她了。entry 里面設置多入口,CommonsChunkPlugin里面再提取。

 1 entry: {
 2   //...
 3   antd: [ //build the mostly used components into a independent chunk,avoid of total package over size.
 4       'antd/lib/button',
 5       'antd/lib/icon',
 6       'antd/lib/breadcrumb',
 7       'antd/lib/form',
 8       'antd/lib/menu',
 9       'antd/lib/input',
10       'antd/lib/input-number',
11       'antd/lib/dropdown',
12       'antd/lib/table',
13       'antd/lib/tabs',
14       'antd/lib/modal',
15       'antd/lib/row',
16       'antd/lib/col'
17   ]
18 },
19 //...
20 new webpack.optimize.CommonsChunkPlugin({
21     names: ['antd'],
22     minChunks: Infinity
23 }),

 

咦?見證奇跡的時候到了,構建后的項目大小居然小了,整整3M,少了36.64%,厲害了。更驚訝的是,峰值內存減少了180M,減少了24.3%,打包時間減少了26s,直接下降到59196ms,減少25%;這牛逼了。

仔細對比一下,發現原來減少的部分並不是我以為的 antd 組件,antd 組件反而在每個打包文件里面的體積都要更大了,大概多了幾kb,而減少的部分卻是一些 _rc 開頭的組件,這 CommonsChunkPlugin 也是厲害,按需加載部分沒有單獨打包起來,反而打包了這些組件背后的引用,如 rc-table。為什么這些組件最后還是沒有完整的打包在antd里面呢?難道每次用的都不同?

1 import { DatePicker } from 'antd';
2 // / babel-plugin-import 會幫助你加載 JS 和 CSS 轉變成下面內容
3 import DatePicker from 'antd/lib/date-picker';  // 加載 JS
4 import 'antd/lib/date-picker/style/css';        // 加載 CSS

 

這沒看來,只是典型的引入組件,以及引入css模塊而已。這是必然會被打包到公共模塊的呀。看了未丑化的代碼,發現用同一個組件的話,生成的不同文件 antd 的組件內容是一樣的,不存在組件內部不一樣導致沒有打包在一起的情況。折騰許久后尚未解決,不曉得有沒有大神知道。

而且 roadhog.js 的方式不允許添加新的入口,只能直接改源代碼。。。這項目要怎么上線呢?難道每次都要自己改一遍?這就是約定和可配置的問題所在了,后面大神的博客也有討論到,最后的思想還是約定為若干模塊,可自選配置,來適合不同的場景。

  • DedupePlugin/OccurrenceOrderPlugin

這兩個功能在webpack里面很常見,以至於已經被移除了,默認加載包含在 webpack 2 里面了。

CommonsChunkPlugin對項目的優化還是很實在的,能減少不必要的打包,不僅是體積,更多的是從內存和時間上。

webpack外引入的優化

前面提到的 webpack-bundle-analyzer 和 webpack-visualizer-plugin 插件就是從 webapck 外部引入的,可以很直觀的看。

externals 的設置在 Vue 項目里面用的比較多,其中主要 externals 的是 axios, Vue, Vonic, Vue-router這些。本身體積也不大,而且作為單頁面應用還是很需要的。

但是到了 Ant Design Pro 項目,由於 Ant Desgin 項目本身 CSS + JS 就要1.5M,對於首屏的影響是顯著的。雖然可以通過瀏覽器緩存/cdn緩存的方式來自然優化,但是首次體驗還是不行,還是按照官網上的介紹來吧。

  • DllPlugin 和 DLLReferencePlugin

按照官網上的介紹:DLLPlugin 和 DLLReferencePlugin 用某種方法實現了拆分 bundles,同時還大大提升了構建的速度。具體原理則是將特定的第三方 NPM 包模塊提前構建再引入就好了。通過在 webpack 外進行配置,DllPlugin 負責配置輸出引用文件 manifest.json,而 DLLReferencePlugin 在webpack的正常配置里面用 manifest.json 就好了。可以避免每次都對 npm 包打包,明明它們就不會改動,直接引用不是更好嗎。

在 roadhog.js 里面實現就有點那個了,按照 sorrycc 作者的意思,在生產環境使用 DllPlugin 是不合適,打包大量的 npm 包后,會延長首屏時間,與按需加載矛盾。這點就和 CommonsChunkPlugin 是相同,都是提取第三方庫,而且 DllPlugin 是一次打包即可,以后重復用引用,而 CommonsChunkPlugin 是每次打包都要重復提取公共部分,那這兩個又有什么區別?

一般 DllPlugin 打的包會包含很多 npm 包,導致體積很大,首次加載自然不好,而且若以后更新某個包,會導致客戶端重新下載整個 DllPlugin 的生成文件,對於產品迭代是不友善的。反觀 CommonsChunkPlugin,一般提取的公共部分體積較小,例如antd主要組件提取,不到500kb,除非大版本升級,否則客戶端是不會重新請求 vendor.js 文件的。

基於上面的觀點 DllPlugin 一般用於提升本地開發的編譯速度,就是啟動項目開發的時候能夠快點。只是一天能夠啟動多少次項目呢,基本都是熱更新為主吧。。。。。這么看好像意義不大,就是開發人員的自 hight 而已。

發現原來roadhog自己也有 DllPlugin 的配置,只要在 config 里面添加 dllPlugin: true 就可以了,當然也是僅僅限於開發環境,肯定不是生產環境。很是方便,這里就不詳細介紹了,感興趣的可以自行看看這個issue

從 webpack 不足出發

  • HappyPack

使用 HappyPack,可以利用 node.js 的多進程能力,來提升構建速度。在 webpack 打包的時候,經常可以看到 CPU 和內存飈的非常高,內存可以理解,但是 CPU 為何會如此之高呢?只能說明有大量計算,而 node.js 的單進程在大量計算面前是單薄的。可以在 webpack 中定義 loader 規則來轉換文件,讓HappyPack來處理這些文件,實現多進程處理轉換。

設置如下:

 1 new HappyPack({
 2     threads: 4,
 3     loaders: [{
 4       loader: 'babel-loader',
 5       options: babelOptions
 6     }],
 7 })
 8 {
 9   test: /\.(js|jsx)$/,
10   include: paths.appSrc,
11   // loader: 'babel',
12   loader: 'happypack/loader',
13   options: babelOptions
14 }

 

只是運行結果卻不讓人滿意,打包時間/內存什么都和原先的數據幾乎相當。難道和 CommonsChunkPlugin 的時候一樣,又是打開方式不正確?於是按照官網說的加個 id 試試,結果立馬報錯,提示AssertionError: HappyPack: plugin for the loader '1' could not be found! Did you forget to add it to the plugin list?,看到有 issue 提出將 loader 里面的 options 改為 query 就可以了,只是官方提示 webpack 2+ 需要使用 options 來代替query ,最后試了一下也是報錯,報錯的根由是 happyloader 沒有獲取到查詢的識別 id。回頭看了下源碼,query = loaderUtils.getOptions(this) || {}這句話不就是獲取 loader 的 option 配置嗎,里面怎么可能有 id 呢?里面就是 babelOptions,不可能有 id 的。接着看 loader-utils 的源碼,這個就是簡單的獲取查詢到的 query,沒有毛病,難道是 HappyPack 用錯了?

折騰好久后,差不多都要放棄了,我定了定神,重新理一遍,看到了 rules 里面的配置:

1 loaders: [{
2   loader: 'happypack/loader?id=js',
3   options: babelOptions
4 }],

 

options 選項是 roadhog 原先就有的,而 laoder 原先是 babel,后面改為了 happypack 的設置。這個時候眼睛一亮 loader 設置里面有個問號 ?,這個不就是 query 嗎?那 options 呢?loader-utils 里面獲取的是這個 query 還是 option?注釋掉試一試?完美成功了。。。。原來如此簡單。

用了 happypack 之后,不能在 rules 里面的相關 loader 中配置 options,相反只能在 happypack 插件中配置 options!

well, 然而什么都沒有變呀,設置了緩存也沒有用,速度/內存什么的都和之前一摸一樣。這個時候看到了(在 roadhog 中嘗試支持happypack)[https://github.com/sorrycc/roadhog/issues/122]里面大神說了社區版本有問題。。。。。。雖然不知道具體的原因,但是實際效果是對 js 文件用 HappyPack 的配置,是沒有起到想象中的多進程計算的優點的,原因或許出在 babel/HappyPack 身上了,最后還是落到了單線程計算上。具體就不分析了,有空可以在研究一下。

  • uglifyPlugin

uglifyPlugin 是生產環境中必備的,畢竟壓縮丑化代碼,不僅可以降低客戶端加載項目體積,降低打開時間,而且可以防止反向編譯工程的可能性。在本文的開頭就提到過,首次優化就是針對 uglifyPlugin 的,而且效果顯著。

使用 webpack.optimize.UglifyJsPlugin 的時候,平均下來 webpack 的構建時間要達到 86s 左右。當不進行代碼壓縮丑化的話,構建時間下降了 68s 左右,並且構建時候,node.js 占用內存峰值下降了 380M 多,可以說不壓縮丑化的話,效果是非常好的。但是項目體積卻基本是原本的三倍之大,這是難以容忍的。webpack自帶的uglifyPlugin,如此笨拙,要如何處理呢?

對 webpack.optimize.UglifyJsPlugin 在里面添加 cache: true 的配置也是沒有什么效果,看了下官網介紹的另外一個 UglifyJsPlugin 插件,上面寫着 webpack =< v3.0.0 已經包含 UglifyjsWebpackPlugin 的 0.4.6 版本了,如果想要安裝最新版本才按照下面介紹的來。發現本地安裝的 webpack 版本是 3.11.0,自然是內置 0.4.6 版本。1.0.0 版本是會在 webpack 4.0.0 里面安排的。那如果直接用 uglifyjs-webpack-plugin 最新版本呢?

安裝 uglifyjs-webpack-plugin 1.2.2,設置配置如下:

 1 new UglifyJsPlugin({
 2   cache: true,
 3   uglifyOptions: {
 4     compress: {
 5       warnings: false
 6     },
 7     output: {
 8       comments: false,
 9       ascii_only: true
10     },
11     ie8: true,
12   }
13 })

 

初次構建的時候,構建時間較之前多40s,也就是多了46.5%,有點誇張的多,內存還好,峰值基本和用 0.4.6 版本的一樣。但是 再次構建呢?構建時間居然下降了68s,而且內存峰值和未用代碼壓縮丑化的時候相似,也就是減少了 380M,實在厲害,牛逼哄哄。

還可以開啟並行,也就是多進程工作,設置 parallel: true,設置之后測試,初次構建時間居然比普通的再次構建時間要少10s,但是問題也很明顯 CPU 在平時的時候峰值基本在 45% 左右,而多進程后,CPU 的峰值居然很長一段時間都在 100%,內存也是達到了 1300+M,實在恐怖,如果正式服這么用不曉得會不會爆炸呢?hahaha。parallel 除了可以設置為 true 以外,還能設置成進程數,於是試了等於 2 的時候,CPU 運行峰值接近 95%,而內存峰值在 1100+M,也算是相對較好的數據,只是 CPU 還是接近於爆表。

對於再次構建 parallel 自然是起不到作用的,這里有不得不提另外一個插件 webpack-parallel-uglify-plugin (下載量比另外一款 webpack-uglify-parallel 多上一倍,肯定使用這個嘛)。試了一下,初次構建基本和 uglifyjs-webpack-plugin 1.2.2 一致,只有構建時間快 7s。

綜上所訴,對於服務器 CPU 豪華的可以考慮平行壓縮丑化,一般時候用 uglifyjs-webpack-plugin 1.2.2多進程就不用設置,使能 cache 就好了,初次構建會慢點,再次構建的話,速度就上天了。

  • UglifyJsPlugin 與 CommonsChunkPlugin

最后自然也是要讓兩者合並試一試,效果如何呢?和為優化之前相比,初次構建,內存減少 120+M,構建時間基本一樣,構建項目大小自然還是少了 3M。咋一看好像不怎么樣,但是要知道這是用上了UglifyJsPlugin,有緩存的!結果再次構建數據如所想的一樣,速度和內存數據,和沒有用代碼壓縮丑化基本一致!

這樣 uglifyjs-webpack-plugin 與 CommonsChunkPlugin 在生產環境自然是很好的選擇。


本文主要是按照(Webpack 構建性能優化探索)[https://github.com/pigcan/blog/issues/1]介紹到的方法實踐

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM