webpack構建和性能優化探索

前言

隨着業務複雜度的不斷的增長,工程模塊的體積也會不斷增長,構建後的模塊一般要以M爲單位計算。在構建過程當中,基於nodejs的webpack在單進程的狀況下loader表現變得愈來愈慢,在不作任何特殊處理的狀況下,構建完後的多項目之間公用基礎資源存在重複打包,基礎庫代碼複用率也不高,這都慢慢暴露出webpack的問題。css

原文地址html

正文

針對存在的問題,社區涌出了各類解決方案,包括webpack自身也在不斷優化。前端

構建優化

下面利用相關的方案對實際項目一步一步進行構建優化,提高咱們的編譯速度,本次優化相關屬性以下:node

  • 機器: Macbook Air 四核 8G內存
  • Webpack: v4.10.2
  • 項目:922個模塊

構建優化方案以下: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-mergewebpack.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版本和項目大小進行了對比測試,以下:

  • Webpack:v2.7.0
  • 項目:1013個模塊
  • 全量production構建:105395ms

添加happypack以後,全量production構建時間下降到58414ms

針對webpack版本:

  • Webpack:v4.23.0
  • 項目:1013個模塊
  • 全量development構建 : 12352ms

添加happypack以後,全量development構建下降到11351ms。

獲得結論:Webpack v4 以後,happypack已經力不從心,效果並不明顯,並且在小型中並不適用。

因此針對並行加載方案要不要加,要具體項目具體分析。

性能優化

對於webpack編譯出來的結果,也有相應的性能優化的措施。方案以下:

  • 減小模塊數量及大小
  • 合理緩存
  • 合理拆包

減小模塊數量及大小

針對減小模塊數量及大小,咱們在構建優化的章節中有提到不少,具體點以下:

  • 按需加載 babel-plugin-import(antd、iview、bizcharts)、babel-plugin-component(element-ui)
  • 減小無用模塊webpack.ContextReplacementPlugin、webpack.IgnorePlugin
  • Tree-shaking:樹搖功能,消除無用代碼,無用模塊。
  • Scope-Hoisting:做用域提高。
  • babel-plugin-transform-runtime,針對babel-polyfill清除沒必要要的polyfill。

前面兩點咱們就不具體描述,在構建優化章節中有說。

Tree-shaking

樹搖功能,將樹上沒用的葉子搖下來,寓意將沒有必要的代碼刪除。該功能在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打印出關於代碼剔除的提示。

Scope-Hoisting

做用域提高,儘量的把打散的模塊合併到一個函數中,前提是不能形成代碼冗餘。所以只有那些被引用了一次的模塊才能被合併。

可能很差理解,下面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-plugin-transform-runtime

在咱們實際的項目中,爲了兼容一些老式的瀏覽器,咱們須要在項目加入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呢?

  • 避免覆蓋舊文件
  • 回滾方便,只須要回滾html
  • 因爲文件名惟一,可開啓服務器永遠緩

而後,webpack對應的hash有兩種,hashchunkhash

  • hash是跟整個項目的構建相關,只要項目裏有文件更改,整個項目構建的hash值都會更改,而且所有文件都共用相同的hash值
  • chunkhash根據不一樣的入口文件(Entry)進行依賴文件解析、構建對應的chunk,生成對應的哈希值。

細想咱們指望的最理想的hash就是當咱們的編譯後的文件,不論是初始化文件,仍是chunk文件或者樣式文件,只要文件內容一修改,咱們的hash就應該更改,而後刷新緩存。惋惜,hash和chunkhash的最終效果都沒有達到咱們的預期。

另外,還有來自於的 extract-text-webpack-plugincontenthash,contenthash針對編譯後的每一個文件內容生成hash。只是extract-text-webpack-plugin在wbepack4中已經被棄用,並且這個插件只對css文件生效。

webpack-md5-hash

爲了達到咱們的預期效果,咱們能夠爲webpack添加webpack-md5-hash插件,這個插件可讓webpack的chunkhash根據文件內容生成hash,相對穩定,這樣就能夠達到咱們預期的效果了,配置以下:

var WebpackMd5Hash = require('webpack-md5-hash');
 
module.exports = {
    // ...
    output: {
        //...
        chunkFilename: "[chunkhash].[id].chunk.js"
    },
    plugins: [
        new WebpackMd5Hash()
    ]
};

合理拆包

爲了減小首屏加載的時候,咱們須要將包拆分紅多個包,而後須要的時候在加載,拆包方案有:

  • 第三方包,DllPlugin、externals。
  • 動態拆包,利用import()、require.ensure()語法拆包
  • splitChunksPlugin

針對第一點第三方包,咱們也在第一章節構建優化中有介紹,這裏就不詳細說了。

動態拆包

首先是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');
splitChunksPlugin

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,僅爲本身有更好的開發體驗,也應該行動起來。

相關文章
相關標籤/搜索