1. 前言
關於webpack
,相信現在的前端開發人員一定不會陌生,因為它已經成為前端開發人員必不可少的一項技能,它的官方介紹如下:
webpack 是一個模塊打包器。webpack的主要目標是將 JavaScript 文件打包在一起,打包后的文件用於在瀏覽器中使用,但它也能夠勝任轉換(transform)、打包(bundle)或包裹(package)任何資源(resource or asset)。
在日常開發工作中,我們除了會使用webpack
以及會編寫它的配置文件之外,我們還需要了解一些關於webpack
性能優化的方法,這樣在實際工作就能夠如虎添翼,增強自身的競爭力。
關於webpack
優化的方法我將其分為兩大類,如下:
- 可以提高
webpack
打包速度,減少打包時間的優化方法 - 可以讓
Webpack
打出來的包體積更小的優化方法
OK,廢話不多說,接下來我們就來分別了解一下優化方法。
2. 提高 Webpack 打包速度
2.1 優化Loader搜索范圍
對於 Loader
來說,影響打包效率首當其沖必屬 Babel
了。因為 Babel
會將代碼轉為字符串生成 AST
,然后對 AST
繼續進行轉變最后再生成新的代碼,項目越大,轉換代碼越多,效率就越低。當然了,我們是有辦法優化的。
首先我們可以優化 Loader 的文件搜索范圍,在使用loader
時,我們可以指定哪些文件不通過loader
處理,或者指定哪些文件通過loader
處理。
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
use: ['babel-loader'],
// 只處理src文件夾下面的文件
include: path.resolve('src'),
// 不處理node_modules下面的文件
exclude: /node_modules/
}
]
}
}
對於 Babel
來說,我們肯定是希望只作用在 JS
代碼上的,然后 node_modules
中使用的代碼都是編譯過的,所以我們也完全沒有必要再去處理一遍。
另外,對於babel-loader
,我們還可以將 Babe
l 編譯過的文件緩存起來,下次只需要編譯更改過的代碼文件即可,這樣可以大幅度加快打包時間。
loader: 'babel-loader?cacheDirectory=true'
2.2 cache-loader緩存loader處理結果
在一些性能開銷較大的 loader
之前添加 cache-loader
,以將處理結果緩存到磁盤里,這樣下次打包可以直接使用緩存結果而不需要重新打包。
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
use: [
'cache-loader',
...loaders
],
}
]
}
}
那這么說的話,我給每個loder
前面都加上cache-loader
,然而凡事物極必反,保存和讀取這些緩存文件會有一些時間開銷,所以請只對性能開銷較大的 loader
使用 cache-loader
。關於這個cache-loader
更詳細的使用方法請參照這里cache-loader
2.3 使用多線程處理打包
受限於Node
是單線程運行的,所以 Webpack
在打包的過程中也是單線程的,特別是在執行 Loader
的時候,長時間編譯的任務很多,這樣就會導致等待的情況。那么我們可以使用一些方法將 Loader
的同步執行轉換為並行,這樣就能充分利用系統資源來提高打包速度了。
2.3.1 HappyPack
happypack ,快樂的打包。人如其名,就是能夠讓Webpack
把打包任務分解給多個子線程去並發的執行,子線程處理完后再把結果發送給主線程。
module: {
rules: [
{
test: /\.js$/,
// 把對 .js 文件的處理轉交給 id 為 babel 的 HappyPack 實例
use: ['happypack/loader?id=babel'],
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css$/,
// 把對 .css 文件的處理轉交給 id 為 css 的 HappyPack 實例
use: ['happypack/loader?id=css']
}
]
},
plugins: [
new HappyPack({
id: 'js', //ID是標識符的意思,ID用來代理當前的happypack是用來處理一類特定的文件的
threads: 4, //你要開啟多少個子進程去處理這一類型的文件
loaders: [ 'babel-loader' ]
}),
new HappyPack({
id: 'css',
threads: 2,
loaders: [ 'style-loader', 'css-loader' ]
})
]
2.3.2 thread-loader
thread-loader ,在worker
池(worker pool)中運行加載器loader
。把thread-loader
放置在其他 loader
之前, 放置在這個 thread-loader
之后的 loader
就會在一個單獨的 worker
池(worker pool)中運行。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
{
loader: "thread-loader",
// 有同樣配置的 loader 會共享一個 worker 池(worker pool)
options: {
// 產生的 worker 的數量,默認是 cpu 的核心數
workers: 2,
// 一個 worker 進程中並行執行工作的數量
// 默認為 20
workerParallelJobs: 50,
// 額外的 node.js 參數
workerNodeArgs: ['--max-old-space-size', '1024'],
// 閑置時定時刪除 worker 進程
// 默認為 500ms
// 可以設置為無窮大, 這樣在監視模式(--watch)下可以保持 worker 持續存在
poolTimeout: 2000,
// 池(pool)分配給 worker 的工作數量
// 默認為 200
// 降低這個數值會降低總體的效率,但是會提升工作分布更均一
poolParallelJobs: 50,
// 池(pool)的名稱
// 可以修改名稱來創建其余選項都一樣的池(pool)
name: "my-pool"
}
},
{
loader:'babel-loader'
}
]
}
]
}
}
同樣,thread-loader
也不是越多越好,也請只在耗時的 loader
上使用。
2.3.3 webpack-parallel-uglify-plugin
在 Webpack3
中,我們一般使用 UglifyJS
來壓縮代碼,但是這個是單線程運行的,也就是說多個js
文件需要被壓縮,它需要一個個文件進行壓縮。所以說在正式環境打包壓縮代碼速度非常慢(因為壓縮JS
代碼需要先把代碼解析成AST
語法樹,再去應用各種規則分析和處理AST
,導致這個過程耗時非常大)。為了加快效率,我們可以使用 webpack-parallel-uglify-plugin
插件,該插件會開啟多個子進程,把對多個文件壓縮的工作分別給多個子進程去完成,但是每個子進程還是通過UglifyJS
去壓縮代碼。無非就是變成了並行處理該壓縮了,並行處理多個子任務,提高打包效率。來並行運行 UglifyJS
,從而提高效率。
在 Webpack4
中,我們就不需要以上這些操作了,只需要將 mode
設置為 production
就可以默認開啟以上功能。代碼壓縮也是我們必做的性能優化方案,當然我們不止可以壓縮 JS
代碼,還可以壓縮 HTML
、CSS
代碼,並且在壓縮 JS
代碼的過程中,我們還可以通過配置實現比如刪除 console.log
這類代碼的功能。
let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
module: {},
plugins: [
new ParallelUglifyPlugin({
workerCount:3,//開啟幾個子進程去並發的執行壓縮。默認是當前運行電腦的cPU核數減去1
uglifyJs:{
output:{
beautify:false,//不需要格式化
comments:false,//不保留注釋
},
compress:{
warnings:false,//在Uglify]s除沒有用到的代碼時不輸出警告
drop_console:true,//刪除所有的console語句,可以兼容ie瀏覽器
collapse_vars:true,//內嵌定義了但是只用到一次的變量
reduce_vars:true,//取出出現多次但是沒有定義成變量去引用的靜態值
}
},
})
]
}
關於該插件更加詳細的用法請參照這里webpack-parallel-uglify-plugin
2.4 DllPlugin&DllReferencePlugin
DllPlugin
可以將特定的類庫提前打包成動態鏈接庫,在一個動態鏈接庫中可以包含給其他模塊調用的函數和數據,把基礎模塊獨立出來打包到單獨的動態連接庫里,當需要導入的模塊在動態連接庫里的時候,模塊不用再次被打包,而是去動態連接庫里獲取。這種方式可以極大的減少打包類庫的次數,只有當類庫更新版本才有需要重新打包,並且也實現了將公共代碼抽離成單獨文件的優化方案。
這里我們可以先將react
、react-dom
單獨打包成動態鏈接庫,首先新建一個新的webpack
配置文件:webpack.dll.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
// 想統一打包的類庫
entry:['react','react-dom'],
output:{
filename: '[name].dll.js', //輸出的動態鏈接庫的文件名稱,[name] 代表當前動態鏈接庫的名稱
path:path.resolve(__dirname,'dll'), // 輸出的文件都放到 dll 目錄下
library: '_dll_[name]',//存放動態鏈接庫的全局變量名稱,例如對應 react 來說就是 _dll_react
},
plugins:[
new DllPlugin({
// 動態鏈接庫的全局變量名稱,需要和 output.library 中保持一致
// 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
path: path.join(__dirname, 'dll', '[name].manifest.json')
})
]
}
然后我們需要執行這個配置文件生成依賴文件:
webpack --config webpack.dll.js --mode development
接下來我們需要使用 DllReferencePlugin
將依賴文件引入項目中
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
// ...省略其他配置
plugins: [
new DllReferencePlugin({
// manifest 就是之前打包出來的 json 文件
manifest:path.join(__dirname, 'dll', 'react.manifest.json')
})
]
}
2.5 noParse
module.noParse
屬性,可以用於配置那些模塊文件的內容不需要進行解析(即無依賴) 的第三方大型類庫(例如jquery
,lodash
)等,使用該屬性讓 Webpack
不掃描該文件,以提高整體的構建速度。
module.exports = {
module: {
noParse: /jquery|lodash/, // 正則表達式
// 或者使用函數
noParse(content) {
return /jquery|lodash/.test(content)
}
}
}
2.6 IgnorePlugin
IgnorePlugin
用於忽略某些特定的模塊,讓 webpack
不把這些指定的模塊打包進去。
module.exports = {
// ...省略其他配置
plugins: [
new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
]
}
webpack.IgnorePlugin()
參數中第一個參數是匹配引入模塊路徑的正則表達式,第二個參數是匹配模塊的對應上下文,即所在目錄名。
2.7 打包文件分析工具
webpack-bundle-analyzer
插件的功能是可以生成代碼分析報告,幫助提升代碼質量和網站性能。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
plugins: [
new BundleAnalyzerPlugin({
generateStatsFile: true, // 是否生成stats.json文件
})
// 默認配置的具體配置項
// new BundleAnalyzerPlugin({
// analyzerMode: 'server',
// analyzerHost: '127.0.0.1',
// analyzerPort: '8888',
// reportFilename: 'report.html',
// defaultSizes: 'parsed',
// openAnalyzer: true,
// generateStatsFile: false,
// statsFilename: 'stats.json',
// statsOptions: null,
// excludeAssets: null,
// logLevel: info
// })
]
}
使用方式:
"generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
"analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 啟動展示打包報告的http服務器
2.8 費時分析
speed-measure-webpack-plugin
,打包速度測量插件。這個插件可以測量webpack
構建速度,可以測量打包過程中每一步所消耗的時間,然后讓我們可以有針對的去優化代碼。
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
// 用smw.wrap()包裹webpack的所有配置項
module.exports =smw.wrap({
module: {},
plugins: []
});
2.9 一些小的優化點
我們還可以通過一些小的優化點來加快打包速度
resolve.extensions
:用來表明文件后綴列表,默認查找順序是['.js', '.json']
,如果你的導入文件沒有添加后綴就會按照這個順序查找文件。我們應該盡可能減少后綴列表長度,然后將出現頻率高的后綴排在前面resolve.alias
:可以通過別名的方式來映射一個路徑,能讓Webpack
更快找到路徑
module.exports ={
// ...省略其他配置
resolve: {
extensions: [".js",".jsx",".json",".css"],
alias:{
"jquery":jquery
}
}
};
3. 減少 Webpack 打包后的文件體積
3.1 對圖片進行壓縮和優化
image-webpack-loader這個loder
可以幫助我們對打包后的圖片進行壓縮和優化,例如降低圖片分辨率,壓縮圖片體積等。
module.exports ={
// ...省略其他配置
module: {
rules: [
{
test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
}
}
]
}
]
}
};
3.2 刪除無用的CSS樣式
有時候一些時間久遠的項目,可能會存在一些CSS
樣式被迭代廢棄,需要將其剔除掉,此時就可以使用purgecss-webpack-plugin
插件,該插件可以去除未使用的CSS
,一般與 glob
、glob-all
配合使用。
注意:此插件必須和CSS
代碼抽離插件mini-css-extract-plugin
配合使用。
例如我們有樣式文件style.css
:
body{
background: red
}
.class1{
background: red
}
這里的.class1
顯然是無用的,我們可以搜索src
目錄下的文件,刪除無用的樣式。
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
module.exports ={
// ...
plugins: [
// 需要配合mini-css-extract-plugin插件
new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`,
{nodir: true}), // 不匹配目錄,只匹配文件
})
}),
]
}
3.3 以CDN方式加載資源
我們知道,一般常用的類庫都會發布在CDN
上,因此,我們可以在項目中以CDN
的方式加載資源,這樣我們就不用對資源進行打包,可以大大減少打包后的文件體積。
以CDN
方式加載資源需要使用到add-asset-html-cdn-webpack-plugin
插件。我們以CDN
方式加載jquery
為例:
const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')
module.exports ={
// ...
plugins: [
new AddAssetHtmlCdnPlugin(true,{
'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
})
],
//在配置文件中標注jquery是外部的,這樣打包時就不會將jquery進行打包了
externals:{
'jquery':'$'
}
}
3.4 開啟Tree Shaking
Tree-shaking
,搖晃樹。顧名思義就是當我們搖晃樹的時候,樹上干枯的沒用的葉子就會掉下來。類比到我們的代碼中就是將沒用的代碼搖晃下來,從而實現刪除代碼中未被引用的代碼。
這個功能在webpack4
中,當我們將mode
設置為production
時,會自動進行tree-shaking
。
來看下面代碼:
main.js
import { minus } from "./calc";
console.log(minus(1,1));
calc.js
import {test} from './test';
export const sum = (a, b) => {
return a + b + 'sum';
};
export const minus = (a, b) => {
return a - b + 'minus';
};
test.js
export const test = ()=>{
console.log('hello')
}
console.log(test());
觀察上述代碼其實我們主要使用minus
方法,test.js
代碼是有副作用的!所謂"副作用",官方文檔如下解釋:
「副作用」的定義是,在導入時會執行特殊行為的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局作用域,並且通常不提供 export。
對上述代碼進行打包后發現'hello'
依然會被打印出來,這時候我們需要在package.json
中配置配置不使用副作用:
{
"sideEffects": false
}
如果這樣設置,默認就不會導入css
文件啦,因為我們引入css
也是通過import './style.css'
這里重點就來了,tree-shaking
主要針對es6模塊,我們可以使用require
語法導入css,但是這樣用起來有點格格不入,所以我們可以配置css
文件不是副作用,如下:
{
"sideEffects":[
"**/*.css"
]
}
3.5 開啟Scope Hoisting
Scope Hoisting
可以讓 Webpack
打包出來的代碼文件更小、運行的更快, 它又譯作 "作用域提升",是在 Webpack3
中新推出的功能。
由於最初的webpack
轉換后的模塊會包裹上一層函數,import
會轉換成require
,因為函數會產生大量的作用域,運行時創建的函數作用域越多,內存開銷越大。而Scope Hoisting 會分析出模塊之間的依賴關系,盡可能的把打包出來的模塊合並到一個函數中去,然后適當地重命名一些變量以防止命名沖突。這個功能在webpack4
中,當我們將mode
設置為production
時會自動開啟。
比如我們希望打包兩個文件
let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)
最終打包后的結果會變成 console.log(6)
,這樣的打包方式生成的代碼明顯比之前的少多了,並且減少多個函數后內存占用也將減少。如果你希望在開發模式development
中開啟這個功能,只需要使用插件 webpack.optimize.ModuleConcatenationPlugin()
就可以了。
module.exports = {
// ...
plugins: [
// 開啟 Scope Hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
]
}
3.6 按需加載&動態加載
必大家在開發單頁面應用項目的時候,項目中都會存在十幾甚至更多的路由頁面。如果我們將這些頁面全部打包進一個文件的話,雖然將多個請求合並了,但是同樣也加載了很多並不需要的代碼,耗費了更長的時間。那么為了首頁能更快地呈現給用戶,我們肯定是希望首頁能加載的文件體積越小越好,這時候我們就可以使用按需加載,將每個路由頁面單獨打包為一個文件。在給單頁應用做按需加載優化時,一般采用以下原則:
- 對網站功能進行划分,每一類一個
chunk
- 對於首次打開頁面需要的功能直接加載,盡快展示給用戶,某些依賴大量代碼的功能點可以按需加載
- 被分割出去的代碼需要一個按需加載的時機
動態加載目前並沒有原生支持,需要babel
的插件:plugin-syntax-dynamic-import
。安裝此插件並且在.babelrc
中配置:
{
// 添加
"plugins": ["transform-vue-jsx", "transform-runtime"],
}
例如如下示例:
index.js
let btn = document.createElement('button');
btn.innerHTML = '點擊加載視頻';
btn.addEventListener('click',()=>{
import(/* webpackChunkName: "video" */'./video').then(res=>{
console.log(res.default);
});
});
document.body.appendChild(btn);
webpack.config.js
module.exports = {
// ...
output:{
chunkFilename:'[name].min.js'
}
}
這樣打包后的結果最終的文件就是 video.min.js
,並且剛啟動項目時不會加載該文件,只有當用戶點擊了按鈕時才會動態加載該文件。
4. 總結
以上就是一些常用的webpack
優化手段,當然webpack
優化手段還有很多,並且用法也有很多。需要的話可以閱讀官方文檔來深入學習。
(完)