詳解webpack構建優化

語言: CN / TW / HK

當專案越來越複雜時,會面臨著構建速度慢和構建出來的檔案體積大的問題。webapck構建優化對於大專案是必須要考慮的一件事,下面我們就從速度和體積兩方面來探討構建優化的策略。

分析工具

在優化之前,我們需要了解一些量化分析的工具,使用它們來幫助我們分析需要優化的點。

webpackbar

webpackbar可以在打包時實時顯示打包進度。配置也很簡單,在plugins陣列中加入即可:

```javascript const WebpackBar = require('webpackbar') module.exports = { plugins: [ ... new WebpackBar() ] }

```

speed-measure-webpack-plugin

使用speed-measure-webpack-plugin可以看到每個loader和plugin的耗時情況。

和普通外掛的使用略有不同,需要用它的wrap方法包裹整個webpack配置項。

```javascript const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') const smp = new SpeedMeasurePlugin() module.exports = smp.wrap({ entry: './src/main.js', ... })

```

打包後,在命令列的輸出資訊如下,我們可以看出哪些loader和plugin耗時比較久,然後對其進行優化。

image,.png

webpack-bundle-analyzer

webpack-bundle-analyzer以視覺化的方式讓我們直觀地看到打包的bundle中到底包含哪些模組內容,以及每一個模組的體積大小。我們可以根據這些資訊去分析專案結構,調整打包配置,進行優化。

在plugins陣列中加入該外掛。構建完成後,預設會在http://127.0.0.1:8888/展示分析結果。

```javascript const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin module.exports = { plugins: [ ... new BundleAnalyzerPlugin() ] }

```

image.png webpack-bundle-analyzer會計算出模組檔案在三種情形下的大小:

  • stat:檔案未經過任何轉換的原始大小
  • parsed:檔案經過轉換後的輸出大小(比如babel-loader轉換ES6->ES5、UglifyJsPlugin壓縮等等)
  • gzip:parsed後的檔案,經過Gzip壓縮的大小 使用speed-measure-webpack-pluginwebpack-bundle-analyzer本身也會增加打包時間(webpack-bundle-analyzer特別耗時),所以建議這兩個外掛在開發分析時使用,而在生產環境去掉。

image.png

優化構建速度

多程序構建

執行在Node.js之上的 Webpack 是單執行緒的,就算有多個任務同時存在,它們也只能一個一個排隊執行。當專案比較複雜時,構建就會比較慢。如今大多數CPU都是多核的,我們可以藉助一些工具,充分釋放 CPU 在多核併發方面的優勢。參考webpack視訊講解:進入學習

比較常見的有happypackthread-loader

happypack

happypack能夠將構建任務分解給多個子程序去併發執行,子程序處理完後再把結果傳送給主程序。使用配置如下,就是把原有的loader的配置轉移到happyPack中去處理。

```javascript const Happypack = require('happypack') module.exports = { module:{ rules:[ { test: /.js$/, use: 'happypack/loader?id=babel' //問號後面的查詢引數指定了處理這類檔案的HappyPack例項的名字 }, ] }, plugins:[ new Happypack({ id: 'babel', //HappyPack例項名,對應上面rules中的“id=babel” use: ['babel-loader'] //原本要使用的loader }) ] }

```

thread-loader

happypack的作者已經沒有這個專案進行維護了,在webpack4之後,可以使用thread-loader

thread-loader使用起來很簡單,就是把它放置在其它loader之前,如下所示。放置在這個thread-loader之後的 loaders會執行在一個單獨的worker池中。

```css module.exports = { module:{ rules:[ { test: /.js$/, use: ['thread-loader','babel-loader'] } ] }, }

```

image.png 如果是小專案,不建議開啟多程序構建,因為開啟程序是需要花費時間的,構建速度反而會變慢。

利用快取

利用快取可以提升二次構建速度(下面的對比圖都是二次構建的速度)。使用快取後,在node_modules中會有一個.cache目錄,用於存放快取的內容。

cache-loader

在一些效能開銷較大的 loader 之前新增此cache-loader,以將結果快取到磁碟中。

```css module.exports = { module: { rules: [ { test: /.js$/, use: ['cache-loader','babel-loader'] } ] } }

```

可以看出,使用cache-loader後,構建速度有非常明顯的提升。

image.pngbabel-loader使用快取,也可以不借助cache-loader,直接在babel-loader後面加上?cacheDirectory=true

```css module.exports = { module: { rules: [ { test: /.js$/, use: ['babel-loader?cacheDirectory=true'] } ] } }

```

hard-source-webpack-plugin

hard-source-webpack-plugin用於開啟模組的快取。

```javascript const HardSourceWebpackPlugin = require("hard-source-webpack-plugin") module.exports = { plugins:[ new HardSourceWebpackPlugin() ] }

```

使用hard-source-webpack-plugin後,二次構建速度大概提升了90%。

image.png

include/exclude

通常來說,loader會處理符合匹配規則的所有檔案。比如babel-loader,會遍歷專案中用到的所有js檔案,對每個檔案的程式碼進行編譯轉換。而node_modules裡的js檔案基本上都是轉譯好了的,不需要再次處理,所以我們用 include/exclude 來幫我們避免這種不必要的轉譯。

```javascript module.exports = { module:{ rules:[ { test: /.js$/, use: ['babel-loader'], exclude: /node_modules/ //或者 include: [path.resolve(__dirname, 'src')] } ] }, }

```

include直接指定查詢資料夾,比exclude效率更高,更能提升構建速度。 image.png

動態連結庫

上面的babel-loader可以通過include/exclude,避免處理node_modules裡的第三方庫。

但如果將第三方庫程式碼和業務程式碼都打包進一個bundle檔案,那麼處理這個bundle檔案的外掛,比如uglifyjs-webpack-plugin、terser-webpack-plugin等,就沒辦法不處理裡面第三方庫內容。

其實第三方庫程式碼基本都是成熟的,不用作什麼處理。因此,我們可以將專案的第三方庫程式碼分離出來。

常見的處理方式有三種:

  1. Externals
  2. SplitChunks
  3. DllPlugin

Externals可以避免處理第三方庫,但每一個第三方庫都得在html文件中增加一個script標籤來引入,一個頁面過多的js檔案下載會影響網頁效能,而且有時我們只使用第三方庫中的一小部分功能,用script標籤全量引入不太合理。

SplitChunks在每一次構建時都會重新構建第三方庫,不能有效提升構建速度。

這裡推薦使用DllPlugin和DLLReferencePlugin(配合使用),它們是webpack的內建外掛。DllPlugin會將不頻繁更新的第三方庫單獨打包,當這些第三方庫版本沒有變化時,就不需要重新構建。

使用方法:

  1. 使用DllPlugin打包第三方庫

  2. 使用DLLReferencePlugin引用manifest.json,去關聯第1步中已經打好的包

  3. 首先,新建一個webpack配置檔案webpack.dll.js用於打包第三方庫(第1步)

```javascript const path = require('path') const webpack = require('webpack')

module.exports = { mode: 'production', entry: { three: ['three', 'dat.gui'] // 第三方庫陣列 }, output: { filename: '[name].dll.js', //[name]就是在entry path: path.resolve(__dirname, 'dist/lib'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: path.resolve(__dirname, 'dist/lib/[name].json') //manifest.json的存放位置 }) ] }

```

打包好後,可以看到,在dist目錄下增加了一個lib資料夾。 image.png

  • 然後,我們在webpack.base.js做一下修改,去關聯第1步中已經打好的包(第2步)

```java module.exports = { plugins:[ //修改CleanWebpackPlugin配置 new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [
'!lib/**' //在每次清楚dist目錄時,不清理lib資料夾的內容 ] }), // dll相關配置 new webpack.DllReferencePlugin({
// 將manifest欄位配置成我們第1步中打包出來的json檔案 manifest: require('./dist/lib/three.json')
}) ] }

```

再次打包後可以看到,相比於一開始整個專案的體積 9.11MB,體積減小了90%,因為這是一個多頁面打包(多頁面打包配置)的應用,每個頁面都引用了體積龐大的three.js核心檔案,我們把體積最大的three.js核心檔案從每個頁面的bundle中抽離出來後,bundle的體積大大減小。

image.png

再來看看構建時間:相比於使用DllPlugin之前,時間減少了30% 。 image.png

不僅僅是第三方庫,業務程式碼中的基礎庫也可以通過進行DllPlugin分離。

優化構建體積

程式碼分割

分離第三方庫和業務程式碼中的基礎庫,可以避免單個bundle.js體積過大,載入時間過長。並且在多頁面構建中,還能減少重複打包。

常見的操作是通過SplitChunks(之前的文章已經詳細地寫過了:SplitChunks)和 動態連結庫(如上所示),這裡都不再贅述。

動態import

動態import的作用主要減少首屏資源的體積,非首屏的資源在用到的時候再去請求,從而提高首屏的載入速度。一個常見的例子就是單頁面應用的路由管理(比如vue-router

```css { path: '/list', name: 'List', component: () => import('../views/List.vue')
},

```

不是直接import元件(import List from '../views/List.vue'),那樣會把元件都打包進同一個bundle。而是動態import元件,凡是通過import()引用的模組都會打包到獨立的bundle,使用到的時候再去載入。對於功能複雜,又不是首屏必須的資源都推薦使用動態import。

```scss show彈窗 / methods: { loadModal(){ import('../modal/index.js') } } /

```

treeShaking

使用ES6的import/export語法,並且使用下面的方式匯入匯出你的程式碼,而不要使用export default。

```javascript // util.js 匯出 export const a = 1 export const b = 2 export function afunc(){} 或 export { a, b, afunc }

// index.js 匯入 import { a, b } from './util.js' console.log(a,b)

```

那麼在mode:production生產環境,就會自動開啟tree-shaking,移除沒有使用到的程式碼,上面例子中的afunc函式就不會被打包到bundle中。

程式碼壓縮

常用的js程式碼壓縮外掛有:uglifyjs-webpack-pluginterser-webpack-plugin

在webpack4中,生產環境預設開啟程式碼壓縮。我們也可以自己配置去覆蓋預設配置,來完成更定製化的需求。

v4.26.0版本之前,webpack內建的壓縮外掛是uglifyjs-webpack-plugin,從v4.26.0版本開始,換成了terser-webpack-plugin。我們這裡也以terser-webpack-plugin為例,和普通外掛使用不同,在optimization.minimizer中配置壓縮外掛

```java const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [
new TerserPlugin({ parallel: true, //開啟並行壓縮,可以加快構建速度 sourceMap: true, //如果生產環境使用source-maps,則必須設定為true }) ] } }

```

image.png

雪碧圖

雪碧圖將多張小圖示拼接成一張大圖,在HTTP1.x環境下,雪碧圖可以減少HTTP請求,加速網頁的顯示速度。

用於合成雪碧圖的圖示體積要小,較大的圖片不建議拼接成雪碧圖;同時要是網站靜態圖示,不是通過ajax請求動態獲取的圖示。所以通常是作為網站logo、icon之類的圖片。

開發時,可以是UI提供雪碧圖,但是每新增一個圖示,就要重新制作一次,重新計算偏移量,比較麻煩。通過webpack外掛合成雪碧圖,就可以在開發時直接使用單個小圖示,在打包時,自動合成雪碧圖,並自動自動修改css中的background-position的值。

下面,我們藉助postcss-sprites來自動合成雪碧圖。

首先,在webpack.base.js中配置postcss-loader

```javascript //webpack.base.js module.exports = { module: { rules: [ { test: /.css$/, use: ['vue-style-loader','css-loader', 'postcss-loader'] //配置postcss-loader }, { test: /.less$/, use: [ 'vue-style-loader','css-loader', 'postcss-loader', 'less-loader'] //配置postcss-loader } ] } };

```

然後在專案根目錄下新建.postcssrc.js,配置postcss-sprites

```javascript module.exports = { "plugins": [ require('postcss-sprites')({ // 預設會合並css中用到的所有靜態圖片 // 使用filterBy指定需要合併的圖片,比如這裡這裡只合並images/icon資料夾下的圖片 filterBy: function (image) { if (image.url.indexOf('/images/icon/') > -1) { return Promise.resolve(); } return Promise.reject(); } }) ] }

```

預設會把圖片合併到名為sprite.png的雪碧圖中。

在css中直接指定小圖示當背景:

```css .star{ display: inline-block; height: 100px; width: 100px; &.l1{ background: url('../icon/star.png') no-repeat; } &.l2{ background: url('../icon/star2.png') no-repeat; } &.l3{ background: url('../icon/star3.png') no-repeat; } }

```

打包完成後可以看到,自動修改了background-imagebackground-position

image.png image.png image.png

gzip

開啟gzip壓縮,可以減小檔案體積。在瀏覽器支援gzip的情況下,可以加快資源載入速度。服務端和客戶端都可以完成gzip壓縮,服務端響應請求時壓縮,客戶端應用構建時壓縮。但壓縮檔案這個過程本身是需要耗費時間和CPU資源的,如果存在大量的壓縮需求,會加大伺服器的負擔。

所以可以在構建打包時候就生成gzip壓縮檔案,作為靜態資源放在伺服器上,接收到請求後直接把壓縮檔案返回。

使用webpack生成gzip檔案需要藉助compression-webpack-plugin,使用配置如下:

```javascript const CompressionWebpackPlugin = require("compression-webpack-plugin") module.exports = { plugins: [ new CompressionWebpackPlugin({ test: /.(js|css)$/, //匹配要壓縮的檔案 algorithm: "gzip" }) ] }

```

打包完成後除了生成打包檔案外,還會額外生成 .gz字尾的壓縮檔案。可以看出,gzip壓縮檔案的體積比未壓縮檔案的體積小很多。

image.png