webpack產(chǎn)出代碼做性能優(yōu)化的效果無非是下面三種效果:
- 體積更小
- 合理分包,不重復(fù)加載
- 速度更快、內(nèi)存使用更少
下面將通過介紹以下方案來達(dá)到以上目的:
- 小圖片使用base64編碼:
url-loader -
bundle加hash:應(yīng)用緩存 - 使用
import懶加載模塊 - 使用
splitChunks提取公共組件 - 使用
IgnorePlugin忽略無用的模塊 - 使用
CDN加速:添加publicPath - 使用
production模式:- 自動(dòng)開啟代碼壓縮,使得打包體積更小
-
Vue,React等會(huì)自動(dòng)刪除調(diào)試代碼(比如開發(fā)環(huán)境的warning警告),體積會(huì)比開發(fā)時(shí)更小 - 啟動(dòng)
tree-shaking,刪除無用的代碼
- 使用作用域提升:
Scope Hosting
其實(shí)里面很多方案,我們?cè)诨A(chǔ)配置和高級(jí)配置時(shí)已經(jīng)提及過:
小圖片base64編碼
通過設(shè)置url-loader的閥值來控制,小于閥值的圖片使用base64編碼,以此來減少http請(qǐng)求
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.(jpg|png|jpeg|gif)$/,
use: {
loader:'url-loader',
options: {
limit: 8 * 1024, //限制 8kb 以下使用base64
esMoudle: false,
name: '[name]-[hash:10].[ext]',
// 打包到/images目錄下
outputPath: 'images'
}
}
}
]
},
bundle 加 hash 值
給產(chǎn)出的文件增加hash值,當(dāng)內(nèi)容沒有變化時(shí),生成的文件名將不會(huì)變化,瀏覽器就會(huì)應(yīng)用緩存,從而提升加載速度
output: {
filename: '[name].[contenthash:8].js',
path: distPath // 輸出目錄
},
懶加載
使用import方式懶加載組件
// index.js
setTimeout(() => {
// 直接使用import導(dǎo)入即可,這樣加載的模塊,相當(dāng)于一個(gè)獨(dú)立的chunk存在
import('./common/dynamicData.js').then(res => {
console.log(res.default.msg);
})
}, 1000)
提取公共代碼
在optimization中配置splitChunks屬性,設(shè)置cacheGroups分組,一般分為兩個(gè)組:verdors(用于提取第三方庫)和common(用于提取自定義的公共模塊)
針對(duì)第三方庫,幾乎不會(huì)改變,提取出來后,改動(dòng)業(yè)務(wù)代碼不會(huì)動(dòng)到第三方庫的打包文件,這樣,就能命中緩存,加快加載速度
針對(duì)公共模塊,只用加載一次,就可以在多個(gè)模塊中使用
// webpack.prod.js
optimization: {
splitChunks: {
chunks: 'all', // 表示要分割的chunk類型:initial只處理同步的; async只處理異步的;all都處理
// 緩存分組
cacheGroups: {
// 第三方模塊
verdors: {
name: 'verdor', // chunk名稱
test: /node_modules/, // 設(shè)置命中目錄規(guī)則
priority: 1, // 優(yōu)先級(jí),數(shù)值越大,優(yōu)先級(jí)越高
minSize: 0, // 小于這個(gè)大小的文件,不分割
minChunks: 1 // 最少復(fù)用幾次,這里意思是只要用過一次就分割出來
},
// 公共模塊
common: {
name: 'common',
minChunks: 2,
priority: 0,
minSize: 0,
minChunks: 2 // 只要引用過2次,就分割成公共代碼
}
}
}
}
IgnorePlugin
IgnorePlugin可以幫助我們忽略某些庫不需要的模塊,從而實(shí)現(xiàn)按需加載,減少打包體積
// webpack.prod.js或webpack.common.js都可以
const webpack = require('webpack')
const prodConfig = {
plugins: [
// webpack4寫法
// new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
// webpack5寫法
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/, // 忽略content設(shè)置的庫中的某個(gè)文件夾
contextRegExp: /moment$/, // 要被忽略某部分內(nèi)容的庫
})
]
}
使用 CDN 加速
在輸出文件配置中將CDN路徑添加至publicPath中(針對(duì)js和css文件);
針對(duì)圖片,也可以在loader的配置中添加publicPath
output: {
// filename: 'bundle.[chunkhash].js', // 輸出文件名,一般要加上hash
filename: '[name].[contenthash:8].js',
path: distPath, // 輸出目錄
publicPath: 'http://cdn.xxx.com'
},
// 或者是將圖片放到cdn中
module: {
rules: [
{
test: /\.(jpg|png|jpeg|gif)$/,
use: {
loader:'url-loader',
options: {
limit: 8 * 1024, //限制 8kb 以下使用base64
esMoudle: false,
name: '[name]-[hash:10].[ext]',
// 打包到/images目錄下
// outputPath: 'images', // 有了publicPath,會(huì)自動(dòng)忽略outputPath,這是針對(duì)放在靜態(tài)資源服務(wù)器上的目錄
publicPath: 'http://cdn.xxx.com'
}
}
}
]
}
這樣打包出來的index.html中的 靜態(tài)資源,會(huì)自動(dòng)添加CND地址前綴
<script defer="defer" src="http://cdn.xxx.com/verdor.03535a89.js"></script>
<script defer="defer" src="http://cdn.xxx.com/index.c6d170da.js"></script>

這一步還需要我們將對(duì)應(yīng)的資源文件給放至對(duì)應(yīng)的CND服務(wù)器地址中
啟用production模式
mode: 'production', // 生產(chǎn)環(huán)境
啟用production模式,會(huì)使打包的體積更小,webpack4以后,只要開啟生產(chǎn)模式,就會(huì)自己幫我們實(shí)現(xiàn)以下功能:
- 自動(dòng)開啟代碼壓縮,比如刪除注釋、空格等,當(dāng)然可以視項(xiàng)目情況,來判斷要不要使用
webpack-parallel-uglify-plugin開啟多進(jìn)程壓縮 -
Vue,React等會(huì)自動(dòng)刪除調(diào)試代碼(比如開發(fā)環(huán)境的warning警告),體積會(huì)比開發(fā)時(shí)更小 - 啟用
tree-shaking,刪除無用的代碼,當(dāng)注意,只有在ES Module才可以使用- ·
ES6 Module是靜態(tài)引入,編譯時(shí)時(shí)就引入模塊的,所以才可以做靜態(tài)分析,實(shí)現(xiàn)tree-shaking -
Commonjs是動(dòng)態(tài)引入的,是執(zhí)行時(shí)才引入,需要執(zhí)行代碼時(shí),才知道引不引入,所以沒法做靜態(tài)分析,也就無法實(shí)現(xiàn)tree-shaking
- ·
Scope Hosting
webpack分析依賴打包出來的文件,一般是一個(gè)模塊生成一個(gè)函數(shù),比如:
// test.js
export default 'module test'
// index.js
import test from './module-test'
console.log(test)
打包后大概是這樣的:
[
function (module, exports, require) {
var module_test = require(1)
console.log(module_test['default'])
},
function (module, exports, require) {
exports['default'] = 'module test'
}
]
這樣,當(dāng)我們引用的模塊很多時(shí),我們打包時(shí)就會(huì)產(chǎn)生很多個(gè)函數(shù),一個(gè)函數(shù)會(huì)生成一個(gè)函數(shù)作用域,當(dāng)多個(gè)模塊時(shí),就會(huì)產(chǎn)生多個(gè)函數(shù)作用域,這樣步驟創(chuàng)建函數(shù)作用域和執(zhí)行函數(shù)、銷毀函數(shù),對(duì)js代碼的執(zhí)行和內(nèi)存是很不友好的
但如果我們按引用順序,把它們合成一個(gè)函數(shù)來執(zhí)行,所有操作都在一個(gè)函數(shù)執(zhí)行,那么性能就會(huì)好很多,比如
// bundle.js
[
function (module, exports, require) {
// ./module-a.js
var module_test_defaultExport = 'module test'
// ./index.js
console.log(module_test_defaultExport)
}
]
它帶來的好處有:
- 減小代碼體積
- 創(chuàng)建的函數(shù)作用域更少
- 代碼可讀性會(huì)更好
但是,Scope Hosting的實(shí)現(xiàn)也是有限制的,它必須是在ES Module環(huán)境下使用
知道了原理,我們來看下Scope Hosting如何配置,它是webpack內(nèi)置的一個(gè)模塊,可以直接使用:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')
resolve: {
// 針對(duì)npm中的第三方庫模塊,優(yōu)先采用jsnext:main中指向ES6模塊化語法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 開啟scope hoisting,作用域提升
new ModuleConcatenationPlugin()
]