隨着業務複雜度的不斷的增長,工程模塊的體積也會不斷增長,構建後的模塊一般要以M爲單位計算。在構建過程當中,基於nodejs的webpack在單進程的狀況下loader表現變得愈來愈慢,在不作任何特殊處理的狀況下,構建完後的多項目之間公用基礎資源存在重複打包,基礎庫代碼複用率也不高,這都慢慢暴露出webpack的問題。css
原文地址html
針對存在的問題,社區涌出了各類解決方案,包括webpack自身也在不斷優化。前端
下面利用相關的方案對實際項目一步一步進行構建優化,提高咱們的編譯速度,本次優化相關屬性以下:node
構建優化方案以下:react
初始構建時間以下:webpack
增量構建 | Development 構建 | Production 構建 | 備註 |
---|---|---|---|
3088ms | 43702ms | 89371ms |
初始構建時候,咱們利用webpack-bundle-analyzer
對編譯結果進行分析,結果以下:git
能夠看到,td-ui(相似於antd的ui組件庫)、moment庫的locale、BizCharts佔了項目的大部分體積,而在沒有所有使用這些庫的所有內容的狀況下,咱們能夠對齊進行按需加載。es6
針對td-ui和BizCharts,咱們對齊添加按需加載babel-plugin-import
,這個包能夠在使用ES6模塊導入的時候,對其進行分析,解析成引入相應文件夾下面的模塊,以下:github
首先,咱們先添加babel的配置,在plugins中加入babel-plugin-import
:web
{ ... "plugins": [ ... ["import", [ { libraryName: 'td-ui', style: true }, { libraryName: 'bizcharts', libraryDirectory: 'lib/components' }, ]] ] }
能夠看到,咱們給bizcharts也添加了按需加載,配置中添加了按需加載的指定文件夾,針對bizcharts,編譯先後代碼對好比下:
編譯前:
編譯後:
注意:bizcharts
按需加載須要引入其核心代碼bizcharts/lib/core
;
到此爲止,td-ui和bizcharts的按需加載已經處理完畢,接下來是針對moment的處理。moment的主要體積來源於locale國際化文件夾,因爲項目中有中英文國際化的需求,咱們這裏使用webpack.ContextReplacementPugin
對該文件夾的上下文進行匹配,只匹配中文和英文的語言包,plugin配置以下:
new webpack.ContextReplacementPugin( /moment[\/\\]locale$/, //匹配文件夾 /zh-cn|en-us/ // 中英文語言包 )
若是沒有國際化的需求,可使用webpack.IgnorePlugin
對整個locale文件夾進行忽略,配置以下:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
減小編譯體積大小完成以後獲得以下構建對比結果:
增量構建 | Development 構建 | Production 構建 | 備註 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 減小編譯體積大小 |
爲了不一些已經編譯好的大型庫從新編譯,咱們須要將這些庫放在編譯意外的地方,或者預先編譯這些庫。
webpack也爲咱們提供了將模塊外鏈的配置externals
,好比咱們把lodash外鏈,配置以下
module.exports = { //... externals : { lodash: 'window._' }, // 或者 externals : { lodash : { commonjs: 'lodash', amd: 'lodash', root: '_' // 指向全局變量 } } };
針對庫預先編譯,webpack也提供了相應的插件,那就是webpack.Dllplugin
,這個插件能夠預先編譯製定好的庫,最後在實際項目中使用webpack.DllReferencePlugin
將預先編譯好的庫關聯到當前的編譯結果中,無需從新編譯。
Dllplugin配置文件webpack.dll.config.js以下:
dllReference配置文件webpack.dll.reference.config.js以下:
最後使用webpack-merge
將webpack.dll.reference.config.js
合併到到webpack配置中。
注意:預先編譯好的庫文件須要在html中手動引入而且必須放在webpack的entry引入以前,不然會報錯。
其實,將大型庫外鏈和將庫預先編譯也屬於減小編譯體積的一種,最後獲得編譯時間結果以下:
增量構建 | Development 構建 | Production 構建 | 備註 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 減小編譯體積大小 |
2246ms | 22870ms | 50601ms | Dll優化後 |
首先,咱們開啓babel-loader自帶的緩存功能(默認其實就是打開的)。
另外,開啓uglifyjs-webpack-plugin
的緩存功能。
添加緩存插件hard-source-webpack-plugin
(固然也能夠添加cache-loader)
const hardSourcePlugin = require('hard-source-webpack-plugin'); moudle.exports = { // ... plugins: [ new hardSourcePlugin() ], // ... }
添加緩存後編譯結果以下:
增量構建 | Development 構建 | Production 構建 | 備註 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 減小編譯體積大小 |
2246ms | 22870ms | 50601ms | Dll優化後 |
1918ms | 10056ms | 17298ms | 使用緩存後 |
能夠看到,編譯效果極好。
因爲nodejs爲單線程,爲了更好利用好電腦多核的特性,咱們能夠將編譯並行開始,這裏咱們使用happypack
,固然也可使用thread-loader
,咱們將babel-loader和樣式的loader交給happypack接管。
babel-loader配置以下:
less-loader配置以下:
構建結果以下:
增量構建 | Development 構建 | Production 構建 | 備註 |
---|---|---|---|
3088ms | 43702ms | 89371ms | |
2561ms | 27864ms | 67441ms | 減小編譯體積大小 |
2246ms | 22870ms | 50601ms | Dll優化後 |
1918ms | 10056ms | 17298ms | 使用緩存後 |
2252ms | 11846ms | 18727ms | 開啓happypack後 |
能夠看到,添加happypack以後,編譯時間有所增長,針對這個結果,我對webpack版本和項目大小進行了對比測試,以下:
添加happypack以後,全量production構建時間下降到58414ms
。
針對webpack版本:
添加happypack以後,全量development構建下降到11351ms。
獲得結論:Webpack v4 以後,happypack已經力不從心,效果並不明顯,並且在小型中並不適用。
因此針對並行加載方案要不要加,要具體項目具體分析。
對於webpack編譯出來的結果,也有相應的性能優化的措施。方案以下:
針對減小模塊數量及大小,咱們在構建優化的章節中有提到不少,具體點以下:
前面兩點咱們就不具體描述,在構建優化章節中有說。
樹搖功能,將樹上沒用的葉子搖下來,寓意將沒有必要的代碼刪除。該功能在webapck V2中已被webpack默認開啓,可是使用前提是,模塊必須是ES6模塊
,由於ES6模塊爲靜態分析,動態引入的特性,可讓webpack在構建模塊的時候知道,那些模塊內容在引入中被使用,那些模塊沒有被使用,而後將沒有被引用的的模塊在轉爲爲AST後刪除。
因爲必須使用ES6模塊,咱們須要將babel的自動模塊轉化功能關閉,不然你的es6模塊將自動轉化爲commonjs模塊,配置以下:
{ "presets": [ "react", "stage-2", [ "env", { "modlues": false // 關閉babel的自動轉化模塊功能,保留ES6模塊語法 } ] ] }
Tree-shaking編譯時候能夠在命令後使用--display-used-exports能夠在shell打印出關於代碼剔除的提示。
做用域提高,儘量的把打散的模塊合併到一個函數中,前提是不能形成代碼冗餘。所以只有那些被引用了一次的模塊才能被合併。
可能很差理解,下面demo對比一下有無Scope-Hoisting的編譯結果。
首先定義一個util.js文件
export default 'Hello,Webpack';
而後定義入口文件main.js
import str from './util.js' console.log(str);
下面是無Scope-Hoisting結果:
而後是Scope-Hoisting後的結果:
與Tree-Shaking相似,使用Scope-Hoisting的前提也是必須是ES6模塊,除此以外,還須要加入webpack內置插件,位於webpack文件夾,webpack/lib/optimize/ModuleConcatenationPlugin
,配置以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { //... plugins: [ new ModuleConcatenationPlugin() ] //... }
另外,爲了更好的利用Scope-Hoisting,針對Npm的第三方模塊,它們也可能提供了ES6模塊,咱們能夠指定優先使用它們的ES6模塊,而不是使用它們編譯後的代碼,webpack的配置以下:
module.exports = { //... resolve: { // 優先採用jsnext:main中指定的ES6模塊文件 mainFields: ['jsnext:main', 'module', 'browser', 'main'] } //... }
jsnext:main
爲業內你們約定好的存放ES6模塊的文件夾,後續爲了規範,更改成module
文件夾。
在咱們實際的項目中,爲了兼容一些老式的瀏覽器,咱們須要在項目加入babel-polyfill這個包。因爲babel-polyfill太大,致使咱們編譯後的包體積增大,下降咱們的加載性能,可是實際上,咱們只須要加入咱們使用到的不兼容的內容的polyfill就能夠,這個時候babel-plugin-transform-runtime
就能夠幫咱們去除那些咱們沒有使用到的polyfill,固然,你須要在babal-preset-env
中配置你須要兼容的瀏覽器,不然會使用默認兼容瀏覽器。
添加babel-plugin-transform-runtime的.babelrc配置以下:
{ "presets": [["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7", "ie >= 9", "chrome >= 52"] // 配置兼容瀏覽器版本 }, "modules": false }], "stage-2"], "plugins": [ "transform-class-properties", "transform-runtime", // 添加babel-plugin-transform-runtime "transform-decorators-legacy" ] }
webpack對應的緩存方案爲添加hash,那咱們爲何要給靜態資源添加hash呢?
而後,webpack對應的hash有兩種,hash
和chunkhash
。
細想咱們指望的最理想的hash就是當咱們的編譯後的文件,不論是初始化文件,仍是chunk文件或者樣式文件,只要文件內容一修改,咱們的hash就應該更改,而後刷新緩存。惋惜,hash和chunkhash的最終效果都沒有達到咱們的預期。
另外,還有來自於的 extract-text-webpack-plugin
的 contenthash
,contenthash針對編譯後的每一個文件內容生成hash。只是extract-text-webpack-plugin在wbepack4中已經被棄用,並且這個插件只對css文件生效。
爲了達到咱們的預期效果,咱們能夠爲webpack添加webpack-md5-hash
插件,這個插件可讓webpack的chunkhash根據文件內容生成hash,相對穩定,這樣就能夠達到咱們預期的效果了,配置以下:
var WebpackMd5Hash = require('webpack-md5-hash'); module.exports = { // ... output: { //... chunkFilename: "[chunkhash].[id].chunk.js" }, plugins: [ new WebpackMd5Hash() ] };
爲了減小首屏加載的時候,咱們須要將包拆分紅多個包,而後須要的時候在加載,拆包方案有:
針對第一點第三方包,咱們也在第一章節構建優化中有介紹,這裏就不詳細說了。
首先是import(),這是webpack提供的語法,webpack在解析到這樣的語法時,會將指定的目錄文件打包成一個chunk,當成異步加載文件輸出到編譯結果中,語法以下:
import(/* webpackChunkName: chunkName */ './chunkFile.js').then(_module => { // do something });
import()遵循promise規範,能夠在then的回調函數中處理模塊。
注意:import()的參數不能徹底是動態的,若是是動態的字符串,須要預先指定前綴文件夾,而後webpack會把整個文件夾編譯到結果中,按需加載。
而後是require.ensure(),與import()相似,爲webpack提供函數,也是用來生成異步加載模塊,只是是使用callback的形式處理模塊,語法以下:
// require.ensure(dependencies: String[], callback: function(require), chunkName: String) require.ensure([], function(require){ const _module = require('chunkFile.js'); }, 'chunkName');
webpack4中,將commonChunksPlugin廢棄,引入splitChunksPlugin,兩個plugin的做用都是用來切割chunk。
webpack 把 chunk 分爲兩種類型,initial和async。在webpack4的默認狀況下,production構建會分析你的 entry、動態加載(import()、require.ensure)模塊,找出這些模塊之間共用的node_modules下的模塊,並將這些模塊提取到單獨的chunk中,在須要的時候異步加載到頁面當中。
默認配置以下:
module.exports = { //... optimization: { splitChunks: { chunks: 'async', // 標記爲異步加載的chunk minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~', // 文件名中chunk的分隔符 name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, // 最小共享的chunk數 priority: -20, reuseExistingChunk: true } } } } };
splitChunksPlugin提供了靈活的配置,開發者能夠根據本身的需求分割chunk,好比下面官方的例子1代碼:
module.exports = { //... optimization: { splitChunks: { cacheGroups: { commons: { name: 'commons', chunks: 'initial', minChunks: 2 } } } } };
意思是在全部的初始化模塊中抽取公共部分,生成一個chunk,chunk名字爲comons。
在如官方例子2代碼:
module.exports = { //... optimization: { splitChunks: { cacheGroups: { commons: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } } };
意思是從全部模塊中抽離來自於node_modules下的全部模塊,生成一個chunk。固然這只是一個例子,實際生產環境中並不推薦,由於會使咱們首屏加載的包增大。
針對官方例子2,咱們能夠在開發環境中使用,由於在開發環境中,咱們的node_modules下的全部文件是基本不會變更的,咱們將其生產一個chunk以後,每次增量編譯,webpack都不會去編譯這個來自於node_modules的已經生產好的chunk,這樣若是項目很大,來源於node_modules的模塊很是多,這個時候能夠大大下降咱們的構建時間。
如今大部分前端項目都是基於webpack進行構建的,面對這些項目,或多或少都有一些須要優化的地方,或許作優化不爲完成KPI,僅爲本身有更好的開發體驗,也應該行動起來。