使用 HappyPack 和 DllPlugin 來提升你的 Webpack 構建速度

Erichain發表於2017-07-04

使用 HappyPack 和 DllPlugin 來提升你的 Webpack 構建速度

@(Blogs)[webpack, Front-End]

本文原文發表在:medium.com/@Erichain/%…
本文采用的 Webpack 版本為 2.0+
本文原始碼地址:github.com/Erichain/we…

如果你問我對 Webpack 什麼印象的話,我只能告訴你,慢,真的慢。即使他的配置如文件所說(當然,它的文件也不是那麼好)很簡單,不像 Grunt 或者 Gulp 那樣需要一堆配置,只需那麼幾十行就能夠配置一個構建系統,我依然覺得,這個構建工具很慢。或許,是從它的文件開始,我就印象不好了?OK,這個話題到此為止,我們開始我們的正題吧。

本篇文章面向的不是 Webpack 新手,如果你對 Webpack 還不太熟悉的話,建議去閱讀它的官方文件。當然,我們肯定也會涉及到一些基礎的東西。

本文重點講解對生產環境的構建的效能的提升,如果需要對本地構建的效能進行提升的話,可以在本文結束之後,自己尋找一下解決方案哦。當然,還有一點需要說明的是,本文中的程式碼在本地不一定真正能夠在瀏覽器中執行,有需要的可以自行搭建本地的構建系統。


一點基礎

使用過 Webpack 的朋友肯定知道,Webpack 的最簡單的配置如下:

module.exports = {
  entry: {
    app: './src/app.js'
  },

  output: {
    path: path.join(__dirname, 'dist-[hash]'),
    filename: '[name].[hash].js'
  }
};複製程式碼

這樣的配置會將我們的檔案打包成為一個 app.[hash].js 檔案。這樣針對的一般是我們的專案不算大的情況,並且公用模組比較少的情況(當然,公用模組較多的話,配置肯定也不會這麼簡單了)。

對於專案中有用到前處理器,ES2015+ 或者其餘的需要編譯後在瀏覽器上執行的語言,我們需要做的就是為這些東西新增上對應的 loader,然後,Webpack 就會自動的幫我們進行處理了(老實說,這一步還是挺方便的)。

一些 loader 配置示例如下:

rules: [
  {
    test: /\.jsx?$/,
    loader: ['babel-loader?presets[]=react,presets[]=latest&compact=false'],
  }, {
    test: /\.scss$/,
    loader: [
      'style-loader',
      'css-loader',
      'postcss-loader',
      'sass-loader'
    ],
  }, {
    test: /\.jpe?g|png|svg|gif/,
    loader: ['url-loader?limit=8192&name=assets/images/[name]-[hash].[ext]'],
  }
]複製程式碼

另外,我們還可以通過一些外掛來更多的定義 Webpack 的打包行為。比如,如果我們有很多第三方庫的引用,並且,多個地方都會引用到這些庫,我們就可以使用 Webpack 的 CommonsChunkPlugin 來將這些公用的程式碼打包成一個檔案(當然,至於速度嘛,我們後面再說),然後,將我們頁面的業務程式碼打包成為一個檔案。

Webpack 的主要配置就這幾項,其他更多的更深入的配置可以檢視 Webpack 的官方文件

速度慢

儘管 Webpack 配置起來很方便,但是,按照一般的配置來的話,構建的速度真的是太慢了,每構建一次都會花掉相當長的時間,這對於開發者們來說簡直是噩夢。

可是,速度為什麼會這麼慢呢?

以我所在的專案為例,由於我們的專案存在多個 entries(大概四十多個),所以,我們的 Webpack 採用的配置是將公用的第三方庫通過 CommonsChunkPlugin 來打包成為一個 common.js

根據這個 common.js 的內容來看,這裡面存放的就是各個 entry 引用的公有的程式碼,比如,我們的很多元件都會用到 React 或者 Redux 這些第三方庫。通過將公有的程式碼單獨打包成一個檔案,然後再將業務程式碼打包成一個檔案,這樣一來,業務程式碼模組本身的體積就會減小很多,頁面的載入速度也能夠得到很大的提升。

雖然這樣打包的方式能夠在一定程度上提升頁面的載入速度,但是,我們簡單的想一想也知道,CommonsChunkPlugin 會去將所有 entry 中的公有模組遍歷出來再進行編譯壓縮混淆,這個過程是非常緩慢的(我們的專案以前在使用這種方式的時候,在這一步會花上至少十二分鐘的時間,你可以想象這個過程有多麼漫長)。

經過了幾個迭代的痛苦的打包上線的過程之後,我們終於不能忍了,決定對這個構建系統進行改造。

改造的過程

說實話,一開始我其實是沒有任何頭緒的,我只知道這個構建的過程慢,但是,並不清楚應該從何處開始進行改造。

與同事們進行了一些商討之後,我準備從以下幾個方面入手:

  • 減少構建的檔案,減小檔案大小:我們的專案中存在太多的無用的檔案和程式碼,我決定先刪除這些無用的東西
  • 移除 CommonsChunkPlugin
  • Search with Google

第一步的作用其實並不明顯,我刪除了很大一部分的無用的圖片和程式碼,但是,構建速度並沒有明顯的提升。

第二步,簡單的移除掉 CommonsChunkPlugin 的話,構建速度確實會快很多,但是,這樣打包出來的專案就不能夠執行了,所以,還需要結合第三步(必須要感謝這個世界存在 Google)。

我在網上找到了許多相關的問題,關鍵性的建議有以下幾個:

  • css-loader 的版本回溯到 0.15 及其以前的版本
  • 使用 HappyPack
  • 使用 DllPlugin

首先,第一點,降低 css-loader 的版本。

在 GitHub 上有這樣一個 issue:0.15.0+ makes Webpack load slowly。按照 issue 中大家的討論,我將我們專案中的 css-loader 的版本降到了 0.14.5。滿懷期待的以為這樣就能夠提升一部分速度,但是,結果是令人失望的——構建的速度並沒有明顯的改變。我試著構建了好幾遍,速度依然沒有提升,所以,第一個方法失敗,我將 css-loader 的版本恢復了回來。

那麼,繼續嘗試第二個方法,也是本文將要重點說明的方法之一,那就是使用 HappyPack。

使用 HappyPack

HappyPack 允許 Webpack 使用 Node 多執行緒進行構建來提升構建的速度。

使用的方法與在 Webpack 中定義 loader 的方法類似,只是說,我們把構建需要的 loader 放到了 HappyPack 中,讓 HappyPack 來為我們進行相應的操作,我們只需要在 Webpack 的配置中引入 HappyPack 的 loader 的配置就好了。

比如,我們編譯 .jsx 檔案的 loader 就可以這樣寫:

new HappyPack({
  id: 'jsx',
  threads: 4,
  loaders: ['babel-loader?presets[]=react,presets[]=latest&compact=false'],
})複製程式碼

其中,threads 指明 HappyPack 使用多少子程式來進行編譯,一般設定為 4 為最佳。

編譯 .scss 檔案的 loader 這樣寫:

new HappyPack({
  id: 'scss',
  threads: 4,
  loaders: [
    'style-loader',
    'css-loader',
    'postcss-loader',
    'sass-loader',
  ],
})複製程式碼

其中,需要注意的一點就是,在使用 HappyPack 的情況下,我們需要單獨建立一個 postcss.config.js 檔案,不然,在編譯的時候,就會報錯。

由於 HappyPack 對 url-loaderfile-loader 的支援度的問題,所以,我們此處,打包圖片檔案的時候,並沒有使用 HappyPack。

postcss.config.js 的配置就像下面這樣(根據你的需求,定製你自己的配置):

module.exports = {
  autoprefixer: {
    browsers: ['last 3 versions'],
  }
};複製程式碼

定義好了我們 HappyPack 的 loader 之後,我們直接在我們的 Webpack 的配置的 plugins 一項中,引入就好了。

那麼,我們在編譯的時候,就會看到下面的輸出:

@HappyPack 輸出|center
@HappyPack 輸出|center

這就是 HappyPack 在編譯的時候的輸出內容。

但是,我們的關注點不是它輸出了什麼,而是說,我們的構建速度有沒有提升。

當然,結果是令人失望的,我們單獨使用 HappyPack 的情況下,構建速度並沒有明顯的提升(當然,或許有所提升但是我沒有發現也有可能)。

所以,為了進一步的提升我們的構建速度,我們將採取第三種方案,那就是 DllPlugin。

使用 DllPlugin

仔細閱讀過 Webpack 文件的朋友肯定對這個外掛會有印象,或者說知道這個外掛是幹嘛用的。其實,我們此處也是基於 Webpack 的文件的一些說明,然後,結合我在專案中的實踐來為大家講解這個外掛。

在 Webpack 中,DllPlugin 並不是單獨的使用的,而是需要與一個名為 DllReferencePlugin 的外掛結合起來使用的。

熟悉 Windows 的朋友就應該知道,DLL 所代表的含義。在 Windows 中,有大量的 .dll 檔案,稱為動態連結庫。

MSDN 上,微軟是這樣解釋動態連結庫的:

A dynamic-link library (DLL) is a module that contains functions and data that can be used by another module (application or DLL).

大概的意思就是說,動態連結庫包含的是,可以在其他模組中進行呼叫的函式和資料。

文件裡面還有一句話是這樣說的:

DLLs provide a way to modularize applications so that their functionality can be updated and reused more easily.

動態連結庫提供了將應用模組化的方式,應用的功能可以在此基礎上更容易被複用。

回到我們的專案中,類似的,我們其實要做的也是將各個模組中公用的部分給打包成為一個公用的模組。這個模組就包含了我們的其他模組中需要的函式和資料(比如,其他元件所需的 React 庫)。

使用 DllPlugin 的時候,會生成一個 manifest.json 這個檔案,所儲存的就是各個模組和所需公用模組的對應關係。

說了這麼多,我們不如直接來看看這個外掛到底是怎麼使用的:

首先,我們需要一個檔案,這個檔案包含所有的第三方或者公用的模組和庫,我們在此將其命名為 vendor.js,檔案的內容如下:

import 'react';
import 'react-dom';複製程式碼

由於我們的示例專案中只用到了這兩個公用的第三方庫,所以,我們此處只需要引入這兩個庫就行了。

在打包的時候,我們將這些公用的模組單獨打包成一個檔案,然後,通過生成的 manifest.json 檔案對應過去。所以,我們需要單獨建立一個 webpack.config.vendor.js

檔案內容其實很簡單:

const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: {
    vendor: [path.join(__dirname, 'src', 'vendor.js')],
  },

  output: {
    path: path.join(__dirname, 'dist-[hash]'),
    filename: '[name].js',
    library: '[name]',
  },

  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      filename: '[name].js',
      name: '[name]',
    }),
  ]
};複製程式碼

可以看到,我們主要的操作是在 plugins 配置中,生成的檔名就是我們所定義的 entry 的名稱,JSON 檔名可以根據自己的需要來命名。像上面這樣,我們就可以將我們的一些公用模組打包出來了。

執行以下命令:

webpack -p --progress --config webpack.config.vendor.js複製程式碼

我們就可以看到這樣的輸出:

@DllPlugin 打包輸出|center
@DllPlugin 打包輸出|center

這樣,我們就完成了構建的第一步。下一步,我們需要在構建應用的配置檔案中,加入我們的 DllPlugin 的配置。

這時候,我們就需要用到 DllReferencePlugin 了。

在我們的主要配置檔案中,加入以下的配置:

const manifest = require('./dll/vendor-manifest.json');

// ... 其他完美的配置

plugins: [
  new webpack.DllReferencePlugin({
    manifest,
  }),
],複製程式碼

就這樣,我們的所有工作就完成了,我們只需要執行一條命令,就能夠看到構建速度的巨大提升。

當然,為了更完美,我們可以將 DllPlugin 和 HappyPack 結合起來使用,效果會更好。具體的程式碼細節,此處不予展示,朋友們可以直接去 GitHub 上檢視。

為了方便構建,我們可以寫一個指令碼將構建過程簡單化。在我的 GitHub 專案裡面有相關的指令碼,包含了一些基礎的操作,有需要的朋友可以去檢視。此處,我們就認為我們的命令可以直接構建了。

為了體現出構建速度的區別,我們先執行 npm run build,這是採用普通方式進行構建的命令。

@採用普通構建方式的構建時間|center
@採用普通構建方式的構建時間|center

可以看到,構建時間為 20353ms,換算下來為 20s 左右。

接下來,我們執行 npm run build dll,通過 DllPlugin 和 HappyPack 進行構建。

@構建 vendor.js 檔案的時間|center
@構建 vendor.js 檔案的時間|center

@構建 app.js 檔案的時間|center
@構建 app.js 檔案的時間|center

我們將兩個時間加起來,總共為 12184ms,換算下來為 12s 左右。快了將近一倍的時間!這還只是檔案少的情況。在我們的實際專案中,構建時間提升了 3 倍多,所以,可以看到 DllPlugin 的強大之處。

一點總結

本文只是尋找了這樣幾種能夠提升構建速度的解決方案,我相信,方法肯定不止這些,一定還有更多的解決方案等待我們去發現。所以,希望各位朋友能夠對本文中不足的地方提出建議,希望與大家共同學習,共同進步。


References

OPTIMIZING WEBPACK FOR FASTER REACT BUILDS

Optimizing Webpack build times and improving caching with DLL bundles

Dynamic-Link Libraries

相關文章