1. 程式人生 > >Webpack 打包優化之速度篇

Webpack 打包優化之速度篇

 

傾城之鏈

在前文 Webpack 打包優化之體積篇中,對如何減小 Webpack 打包體積,做了些探討;當然,那些法子對於打包速度的提升,也是大有裨益。然而,打包速度之於開發體驗及時構建,相當重要;所以有必要對其做更為深入的研究,以便完善工作流,這就是本文存在的緣由。

Webpack Package optimizationWebpack Package optimization


隨著時間的推移,Webpack 也在不斷的優化迭代;截至目前,已經更新至 v4.8.*;在 Webpack4 這個版本,她在原有基礎上,做了很多優化,也引入了頗多的新特性。在新的版本中,將獲得更多模組型別及 對 .mjs

 的支援,更好的預設值、更為簡潔的模式設定、更加智慧的來分割 Chunk,還新增的 splitChunks 來自定義分割程式碼塊,諸此等等。在升級至新版 Webpack 的專案中,在包的構建速度程式碼塊體積 & 數量、以及執行效率,都會有一個質的飛躍;關於對 Webpack 最新諮詢與教程資源,推薦關注 Webpack Tutorial。經過諸多次嘗試和探究後,已將其各種配置、優化經驗,融入 Vue Boilerplate Template 中,並且附有註解說明,希望可以幫助到正準備升級優化 Webpack
 的朋友們,當然也歡迎提出你寶貴的意見和建議([email protected]) 。


減小檔案搜尋範圍

在使用實際專案開發中,為了提升開發效率,很明顯你會使用很多成熟第三方庫;即便自己寫的程式碼,模組間相互引用,為了方便也會使用相對路勁,或者別名(alias);這中間如果能使得 Webpack 更快尋找到目標,將對打包速度產生很是積極的影響。於此,我們需要做的即:減小檔案搜尋範圍,從而提升速度;實現這一點,可以有如下兩法:

配置 resolve.modules

Webpack的resolve.modules配置模組庫(即 node_modules

)所在的位置,在 js 裡出現 import 'vue' 這樣不是相對、也不是絕對路徑的寫法時,會去 node_modules 目錄下找。但是預設的配置,會採用向上遞迴搜尋的方式去尋找,但通常專案目錄裡只有一個 node_modules,且是在專案根目錄,為了減少搜尋範圍,可以直接寫明 node_modules 的全路徑;同樣,對於別名(alias)的配置,亦當如此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    modules: [
      resolve('src'),
      resolve('node_modules')
    ],
    alias: {
      'vue$': 'vue/dist/vue.common.js',
      'src': resolve('src'),
      'assets': resolve('src/assets'),
      'components': resolve('src/components'),
      // ...
      'store': resolve('src/store')
    }
  },
  ...
}

需要額外補充一點的是,這是 Webpack2.* 以上的寫法。在 1.* 版本中,使用的是 resolve.root,如今已經被棄用為 resolve.modules;同時被棄用的還有resolve.fallbackresolve.modulesDirectories

設定 test & include & exclude

Webpack 的裝載機(loaders),允許每個子項都可以有以下屬性:

test:必須滿足的條件(正則表示式,不要加引號,匹配要處理的檔案)
exclude:不能滿足的條件(排除不處理的目錄)
include:匯入的檔案將由載入程式轉換的路徑或檔案陣列(把要處理的目錄包括進來)
loader:一串“!”分隔的裝載機(2.0版本以上,”-loader”不可以省略)
loaders:作為字串的裝載器陣列

對於include,更精確指定要處理的目錄,這可以減少不必要的遍歷,從而減少效能損失。同樣,對於已經明確知道的,不需要處理的目錄,則應該予以排除,從而進一步提升效能。假設你有一個第三方元件的引用,它肯定位於node_modules,通常它將有一個 src 和一個 dist 目錄。如果配置 Webpack 來排除 node_modules,那麼它將從 dist 已經編譯的目錄中獲取檔案。否則會再次編譯它們。故而,合理的設定 include & exclude,將會極大地提升 Webpack 打包優化速度,比如像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module: {
  preLoaders: [
    {
      test: /\.js$/,
      loader: 'eslint',
      include: [resolve('src')],
      exclude: /node_modules/
    },
    {
      test: /\.svg$/,
      loader: 'svgo?' + JSON.stringify(svgoConfig),
      include: [resolve('src/assets/icons')],
      exclude: /node_modules/
    }
  ],
  loaders: [
    {
      test: /\.vue$/,
      loader: 'vue-loader',
      include: [resolve('src')],
      exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/
    },
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: 'url',
      exclude: /assets\/icons/,
      query: {
        limit: 10000,
        name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    }
  ]
}

增強程式碼程式碼壓縮工具

Webpack 預設提供的 UglifyJS 外掛,由於採用單執行緒壓縮,速度頗慢 ;推薦採用 webpack-parallel-uglify-plugin 外掛,她可以並行執行 UglifyJS 外掛,更加充分而合理的使用 CPU 資源,這可以大大減少的構建時間;當然,該外掛應用於生產環境而非開發環境,其做法如下,

1
2
3
4
5
6
new webpack.optimize.UglifyJsPlugin({
  compress: {
    warnings: false
  },
  sourceMap: true
})

 

替換如上自帶的 UglifyJsPlugin 寫法為如下配置即可:

1
2
3
4
5
6
7
8
9
10
11
12
var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
new ParallelUglifyPlugin({
  cacheDir: '.cache/',
  uglifyJS:{
    output: {
      comments: false
    },
    compress: {
      warnings: false
    }
  }
})

當然也有其他同類型的外掛,比如:webpack-uglify-parallel,但根據自己實踐效果來看,並沒有 webpack-parallel-uglify-plugin 表現的那麼卓越,有興趣的朋友,可以更全面的做下對比,擇優選用。需要額外說明的是,webpack-parallel-uglify-plugin 外掛的運用,會相對 UglifyJsPlugin 打出的包,看起來略大那麼一丟丟(其實可以忽略不計);如果在你使用時也是如此,那麼在打包速度跟包體積之間,你應該有自己的抉擇。

用 Happypack 來加速程式碼構建

你知道,Webpack 中為了方便各種資源和型別的載入,設計了以 loader 載入器的形式讀取資源,但是受限於 nodejs 的程式設計模型影響,所有的 loader 雖然以 async 的形式來併發呼叫,但是還是執行在單個 node 的程序,以及在同一個事件迴圈中,這就直接導致了些問題:當同時讀取多個loader檔案資源時,比如`babel-loader`需要 transform 各種jsx,es6的資原始檔。在這種同步計算同時需要大量耗費 cpu 運算的過程中,node的單程序模型就無優勢了,而 Happypack 就是針對解決此類問題而生的存在。

Webpack-HappypackWebpack-Happypack

Happypack 的處理思路是:將原有的 webpack 對 loader 的執行過程,從單一程序的形式擴充套件多程序模式,從而加速程式碼構建;原本的流程保持不變,這樣可以在不修改原有配置的基礎上,來完成對編譯過程的優化,具體配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var HappyPack = require('happypack');
var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module: {
  loaders: [
    {
      test: /\.js[x]?$/,
      include: [resolve('src')],
      exclude: /node_modules/,
      loader: 'happypack/loader?id=happybabel'
    }
  ]
},
plugins: [
  new HappyPack({
    id: 'happybabel',
    loaders: ['babel-loader'],
    threadPool: happyThreadPool,
    cache: true,
    verbose: true
  })
]

可以研究看到,通過在 loader 中配置直接指向 happypack 提供的 loader,對於檔案實際匹配的處理 loader,則是通過配置在 plugin 屬性來傳遞說明,這裡 happypack 提供的 loader 與 plugin 的銜接匹配,則是通過id=happybabel來完成。配置完成後,laoder的工作模式就轉變成了如下所示:

Webpack-HappypackWebpack-Happypack

Happypack 在編譯過程中,除了利用多程序的模式加速編譯,還同時開啟了 cache 計算,能充分利用快取讀取構建檔案,對構建的速度提升也是非常明顯的;更多關於 happyoack 箇中原理,可參見 @淘寶前端團隊(FED) 的這篇:happypack 原理解析。如果你使用的 Vue.js 框架來開發,也可參考 vue-webpack-happypack 相關配置。

設定 babel 的 cacheDirectory 為true

babel-loader is slow! 所以不僅要使用excludeinclude,儘可能準確的指定要轉化內容的範疇,而且要充分利用快取,進一步提升效能。babel-loader 提供了 cacheDirectory特定選項(預設 false):設定時,給定的目錄將用於快取載入器的結果。

未來的 Webpack 構建將嘗試從快取中讀取,以避免在每次執行時執行潛在昂貴的 Babel 重新編譯過程。如果值為空(loader: ‘babel-loader?cacheDirectory’)或true(loader: babel-loader?cacheDirectory=true),node_modules/.cache/babel-loader 則 node_modules 在任何根目錄中找不到任何資料夾時,載入程式將使用預設快取目錄或回退到預設的OS臨時檔案目錄。實際使用中,效果顯著;配置示例如下:

1
2
3
4
5
6
7
8
9
rules: [
  {
    test: /\.js$/,
    loader: 'babel-loader?cacheDirectory=true',
    exclude: /node_modules/,
    include: [resolve('src'), resolve('test')]
  },
  ... ...
]

設定 noParse

如果你確定一個模組中,沒有其它新的依賴,就可以配置這項, Webpack 將不再掃描這個檔案中的依賴,這對於比較大型類庫,將能促進效能表現,具體可以參見以下配置:

1
2
3
4
5
6
7
module: {
  noParse: /node_modules\/(element-ui\.js)/,
  rules: [
    {
      ...
    }
}

拷貝靜態檔案

在前文 Webpack 打包優化之體積篇中提到,引入 DllPlugin 和 DllReferencePlugin 來提前構建一些第三方庫,來優化 Webpack 打包。而在生產環境時,就需要將提前構建好的包,同步到 dist 中;這裡拷貝靜態檔案,你可以使用 copy-webpack-plugin 外掛:把指定資料夾下的檔案複製到指定的目錄;其配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var CopyWebpackPlugin = require('copy-webpack-plugin')

plugins: [
  ...
  // copy custom static assets
  new CopyWebpackPlugin([
    {
      from: path.resolve(__dirname, '../static'),
      to: config.build.assetsSubDirectory,
      ignore: ['.*']
    }
  ])
]

當然,這種工作,實現的法子很多,比如可以藉助 shelljs,可以參見這裡的實現 vue-boilerplate-template