詳解webpack構建優化
當專案越來越複雜時,會面臨著構建速度慢和構建出來的檔案體積大的問題。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耗時比較久,然後對其進行優化。
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() ] }
```
webpack-bundle-analyzer
會計算出模組檔案在三種情形下的大小:
- stat:檔案未經過任何轉換的原始大小
- parsed:檔案經過轉換後的輸出大小(比如babel-loader轉換ES6->ES5、UglifyJsPlugin壓縮等等)
- gzip:parsed後的檔案,經過Gzip壓縮的大小
使用
speed-measure-webpack-plugin
和webpack-bundle-analyzer
本身也會增加打包時間(webpack-bundle-analyzer
特別耗時),所以建議這兩個外掛在開發分析時使用,而在生產環境去掉。
優化構建速度
多程序構建
執行在Node.js之上的 Webpack 是單執行緒的,就算有多個任務同時存在,它們也只能一個一個排隊執行。當專案比較複雜時,構建就會比較慢。如今大多數CPU都是多核的,我們可以藉助一些工具,充分釋放 CPU 在多核併發方面的優勢。參考webpack視訊講解:進入學習
比較常見的有happypack
、thread-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'] } ] }, }
```
如果是小專案,不建議開啟多程序構建,因為開啟程序是需要花費時間的,構建速度反而會變慢。
利用快取
利用快取可以提升二次構建速度(下面的對比圖都是二次構建的速度)。使用快取後,在node_modules中會有一個.cache
目錄,用於存放快取的內容。
cache-loader
在一些效能開銷較大的 loader 之前新增此cache-loader
,以將結果快取到磁碟中。
```css module.exports = { module: { rules: [ { test: /.js$/, use: ['cache-loader','babel-loader'] } ] } }
```
可以看出,使用cache-loader
後,構建速度有非常明顯的提升。
對babel-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%。
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效率更高,更能提升構建速度。
動態連結庫
上面的babel-loader可以通過include/exclude,避免處理node_modules裡的第三方庫。
但如果將第三方庫程式碼和業務程式碼都打包進一個bundle檔案,那麼處理這個bundle檔案的外掛,比如uglifyjs-webpack-plugin、terser-webpack-plugin等,就沒辦法不處理裡面第三方庫內容。
其實第三方庫程式碼基本都是成熟的,不用作什麼處理。因此,我們可以將專案的第三方庫程式碼分離出來。
常見的處理方式有三種:
- Externals
- SplitChunks
- DllPlugin
Externals可以避免處理第三方庫,但每一個第三方庫都得在html文件中增加一個script標籤來引入,一個頁面過多的js檔案下載會影響網頁效能,而且有時我們只使用第三方庫中的一小部分功能,用script標籤全量引入不太合理。
SplitChunks在每一次構建時都會重新構建第三方庫,不能有效提升構建速度。
這裡推薦使用DllPlugin和DLLReferencePlugin(配合使用),它們是webpack的內建外掛。DllPlugin會將不頻繁更新的第三方庫單獨打包,當這些第三方庫版本沒有變化時,就不需要重新構建。
使用方法:
-
使用DllPlugin打包第三方庫
-
使用DLLReferencePlugin引用manifest.json,去關聯第1步中已經打好的包
- 首先,新建一個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資料夾。
- 然後,我們在
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的體積大大減小。
再來看看構建時間:相比於使用DllPlugin之前,時間減少了30% 。
不僅僅是第三方庫,業務程式碼中的基礎庫也可以通過進行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-plugin
和 terser-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
})
]
}
}
```
雪碧圖
雪碧圖將多張小圖示拼接成一張大圖,在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-image
和background-position
。
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壓縮檔案的體積比未壓縮檔案的體積小很多。