roadhog 構建優化

背景

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

roadhog.js問題

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

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

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

優化webpack

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

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

從環境出發這一點,是由於不一樣的nodejs版本和npm版本,有着顯著的性能差別來的。能夠這麼認爲最新版本的nodejs/npm天然有更優秀的性能。因爲項目自己用的就是最新版本的環境,因此這裏也不加以分析了。ios

從項目中出發

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

圖挺好看的,乍一看沒有什麼特別的地方,好像每一個打包文件都是由諸多細文件組成的。並從文件大小來看壓縮事後都在1M如下,無可厚非。可是細心對比下,仍是有很多發現。github

案例1:爲了實現小功能而引用大型 libweb

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

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]介紹到的方法實踐

相關文章
相關標籤/搜索