嘮叨幾句
使用webpack也有一段時間了,從webpack1到webpack3,這中間有太多的曲折與糾結。webpack無疑是強大的,強大到沒有朋友,webpack的設計初衷也是偉大的,它可以編譯我們的代碼,藉助各種loader,使得我們可以使用各種es6/es7的特性,可以使用sass/less,可以編寫react代碼等等。
webpack有很多我們耳熟能詳的功能,例如webpack-dev-server, Hot Module Replacement, SourceMap, UglifyJsPlugin, 自動產生html等等,一切都是配置,那麼這麼多的配置項也帶來了設定檔雜亂可讀性差的問題,而最讓人難以忍受的還是webpack打包的效能問題。
近期接觸到一個工程,這個工程模組有100多個,在全量build的時候,一定會報記憶體溢出,打包失敗。即便是模組不太多的工程,也會發現打包速度十分緩慢的問題,從此便可以名正言順的趁著build的過程去個廁所,喝杯水什麼的。
基於效能這一痛點,然後就開始了折騰,想著法對構建的效能做一些提升。
折騰之路
版本升級
隨著webpack版本的升級,webpack的配置項開始出現了限制,2.0版本以後webpack的配置項已經不允許出現自訂的屬性了,核心就圍繞在entry, output, loaders, 和plugins這幾項當中,這在一定程度上能讓webpack設定檔看起來不那麼的亂。
webpack官方也注意到了效能問題,所以在2.0版本以後,很多在webpack1中需要配置的一些提升效能的外掛程式也都做了預設開啟,例如OccurrenceOrderPlugin(排序輸出), DedupePlugin(重複資料刪除資料)等等,loaders也將預設開啟緩衝。
2.0以後的版本無論是從最佳化配置,到向es6 module,Promise等標準接軌,再到編譯環境和效能的最佳化,再到API設計的整體規範性上,相對1.0版本的改進還是非常顯著的。
3.0版本的webpack加入了範圍提升的功能,老版本webpack需要將每個模組包裹在單獨的函數閉包中實現模組系統。而這些封裝函數往往會使得瀏覽器中啟動並執行javaScript代碼效能有所下降。而 webpack3中提供了外掛程式來允許開發人員啟用範圍提升特性來避免這種額外的效能損耗:
...plugins: [ new webpack.optimize.ModuleConcatenationPlugin()]...
範圍提升特效能夠移除模組的封裝函數,所以引入該外掛程式後打包後代碼的體積也會有所減少。所以升級webpack是提升效能的關鍵。
減少依賴
webpack是把所有的靜態資源打包到一個bundle中,通過各種loader對代碼進行編譯,那麼如果依賴的模組過多,這肯定會導致效能下降。所以我們應該盡量減少依賴以及刪除那些沒有使用到的依賴。
模組 和 依賴位置
通過設定webpack的resolve屬性,來指定所負載檔案的副檔名以及配置模組庫(node_modules)所在的位置,通過配置alias指定路徑別名,在代碼中盡量使用alias的路徑可能提升檔案尋找速度。webpack配置loader的過程中也可以指定檔案路徑和排除檔案路徑。這在一定程度上能讓檔案尋找快起來。
...resolve: { extensions: ['.js'], alias: { globalLib: path.join(__dirname, './src/lib') }, modules: [path.resolve(__dirname, 'node_modules')],},...module: { rules: [ ... { test: /\.js?$/, exclude: /node_modules/, include: path.resolve(__dirname, 'src'), use: [ { loader: 'happypack/loader', options: { id: 'js', }, }, ], }, ... ]},...
開發環境和線上環境分開配置
開發環境和線上環境的代碼要求是不一致的,開發環境更為嚴苛,可是在開發環境完全不需要這麼做,有些外掛程式是十分消耗效能的,那麼在開發環境中我們不需要引用,例如optimize-css-assets-webpack-plugin, UglifyJsPlugin, assets-webpack-plugin, DllReferencePlugin等等,這些代碼最佳化壓縮層面的外掛程式線上上環境更加重要,那麼開發環境完全可以不去載入。我們可以通過定義環境變數NODE_ENV=production的方式,在設定檔中做判斷。
...const NODE_ENV = process.env.NODE_ENV;if(NODE_ENV === 'production'){ config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { unused: true, dead_code: true, warnings: false, }, sourceMap: false, parallel: { cache: true, workers: os.cpus().length, }, minimize: true, mangle: { except: ['$', 'exports', 'require'], }, output: { ascii_only: true, }, }), )}...
打包DLL
通過使用 DllPlugin & DllReferencePlugin 組件,我們可以對一些公用的庫進行獨立打包,單獨引用,這樣在其它代碼進行構建的過程中就不需要再去重新編譯了。DllPlugin產生vendor.js/vendor.css和manifest.json,vendor.js/vendor.css中儲存著打包好的庫檔案,vendor.js中暴漏到全域一個類似require功能的函數,而manifest.json儲存著依賴的資源路徑以及id,提供給DllReferencePlugin使用,DllReferencePlugin在處理檔案的時候,遇到manifest.json中的資源,使用vendor.js暴漏給全域的方法進行處理,而不會把這個檔案打包進來。
// webpack.dll.js...const vendors = [ 'react',];...module.exports = { output: { path: path.resolve('build'), filename: '[name].js', library: '[name]', }, entry: { vendor: vendors, }, ... plugins: [ ... new webpack.DllPlugin({ path: 'manifest.json', name: '[name]', context: __dirname, }) ... ], ...};// webpack.config.js...plugins: [ ... new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./manifest.json'), name: 'vendor', }) ...],...
使用happypack
happypack可以配置編譯緩衝,以及充分利用多進程對靜態資源進行編譯,這在極大程度上提高了代碼的構建速度。這裡推薦一篇文章happypack 原理解析
// @file webpack.config.jsexports.plugins = [ new HappyPack({ id: 'jsx', threads: 4, loaders: [ 'babel-loader' ] }), new HappyPack({ id: 'coffeescripts', threads: 2, loaders: [ 'coffee-loader' ] })];exports.module.loaders = [ { test: /\.js$/, loaders: [ 'happypack/loader?id=jsx' ] }, { test: /\.coffee$/, loaders: [ 'happypack/loader?id=coffeescripts' ] },]
利用緩衝
充分的利用各種可緩衝配置,上述的happypack可緩衝,UglifyJsPlugin可緩衝,每個loader可以配置緩衝等等。
...const os = require('os');const HappyPack = require('happypack');...const createHappyPlugin = function (id, loaders) { return new HappyPack({ id, loaders, threadPool: HappyPack.ThreadPool({ size: os.cpus().length }), cache: true, verbose: true, debug: true, });};...plugins: [ ... createHappyPlugin('js', ['babel-loader?cacheDirectory']) ... new webpack.optimize.UglifyJsPlugin({ compress: { unused: true, dead_code: true, warnings: false, }, sourceMap: false, parallel: { cache: true, workers: os.cpus().length, }, minimize: true, mangle: { except: ['$', 'exports', 'require'], }, output: { ascii_only: true, }, })]
提取公用代碼
使用CommonsChunkPlugin這個外掛程式,可以把資源中引用的公用代碼提取出來,然後我們就可以在頁面中進行單獨引用。使得工具代碼和業務代碼可以分離。加之瀏覽器的快取檔案的能力,也可以提高線上頁面載入的速度。
...plugins: [ ... // 把所有入口檔案的公用代碼提取出來,產生common.js new webpack.optimize.CommonsChunkPlugin('common.js'), ...],...
代碼分割
通過使用require.ensure()對代碼進行按需載入,webpack會收集ensure中的依賴,打包到一個單獨的檔案中,在用到的時候通過jsonp的形式非同步載入進來,通過這種方式進行代碼分割以及按需載入,對頁面載入效能提升有不小的協助,但是這個方法需要修改代碼,不太適用於老工程改造。
CSS分割
使用extract-text-webpack-plugin對css檔案進行抽離,單獨引用,這樣做可以減小bundle的size,另外css檔案是需要載入到dom元素上面,即header中的,無論怎麼說,這樣做都是明智的。
fast-sass-loader
這是一個高效能的sass loader,在比較大的sass工程中,速度是sass-loader的5-10倍。
hash值
通過配置hash值,使用每個檔案內容計算出的md5作為檔案的版本號碼,可以極大程度的使用瀏覽器緩衝,提升頁面效能,通過assets-webpack-plugin這個外掛程式,可以將hash值產生json檔案,方便我們配置。後面介紹這幾個,其實是從頁面載入效能來考慮問題的,這裡也一併提出來了。
知己知彼
webpack構建分析
如果我們對webpack構建的速度不是很滿意,我們可以藉助webpack-analyse或者webpack-visualizer工具對構建過程進行分析,以便於進一步的最佳化,做到知己知彼百戰不殆,開始之前我們需要使用如下命令先產生一個構建過程的json檔案:
webpack --json --profile > stats.json
接下來的過程是可視化的,我們把json檔案上傳,緊接著就可以看到構建結果了。
隨便聊聊
通過以上的最佳化,對webpack構建的速度有不小的提升。完成這個項目後,對改造前後打包時間做了對比,速度提升了十幾倍。但是我想這並不是終極方案,結果也並不是我滿意的。相信後續的webpack版本,能夠讓我們擁有一個配置簡單,構建高效,功能強大的工具。文章中有錯誤的地方,歡迎指出,互相學習,一起進步。Best Regards !