今天為了更好地了解一下Webpack打包優(yōu)化的一些內(nèi)容,看了一下NEXT公開課,Webpack打包極限優(yōu)化,感興趣的朋友可以去騰訊課堂看看,我這里也是對于公開課的筆記總結(jié)!
其中講到的點(diǎn)如下所示:
WebPack基礎(chǔ)知識
構(gòu)建速度優(yōu)化
案例優(yōu)化效果對比
如何分析頁面打包問題?
構(gòu)建體積優(yōu)化
總結(jié)和展望
01|為什么需要構(gòu)建工具?
- 轉(zhuǎn)換ES6語法
- 轉(zhuǎn)換JSX
- CSS前綴補(bǔ)全/預(yù)處理器
- 壓縮混淆
- 圖片壓縮
其中對應(yīng)的瀏覽器的支持情況,涉及到了 CANIUSE中的兼容處理!
我們可以通過最基本的例子來演示:
- WebPack腳本
const path = require("path");
module.exports = {
mode:'production',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'dist'),
filename:'bundle.js'
}
}
- 構(gòu)建結(jié)果
<!DOCTYPE html>
<html>
<head>
<title>example</title>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>
其實(shí)通過Webpack的腳本很好看出具體做了什么?
mode就是制定腳本的運(yùn)行環(huán)境,對應(yīng)的entry就是表示的入口文件,output則是指定編譯的目錄和文件!
執(zhí)行流程
Entry Output
app/index.js Webpack/Plugins/Loaders/ dist/app.js
| 對應(yīng)的loader進(jìn)行處理 |
| |
| Split dist/0.js
app/component.js
|
|
app/util.js
對應(yīng)的定位就是模塊打包器!
Entry對應(yīng)的入口,根據(jù)對應(yīng)的entry就可以知道對應(yīng)的依賴樹,比如說 index.js依賴 component.js component.js依賴util.js
對應(yīng)的Plugin和Loader有什么區(qū)別嗎?
- Loader主要是做資源的解析操作
- Plugin更加強(qiáng)大,為Webpack的拓展,可以做很多l(xiāng)oader沒法做的能夠增強(qiáng)Webpack的功能!
目標(biāo)代碼:
- dist文件中
- 為了更好的加載會將文件做分割以便更好地做懶加載!
02|Grunt,Gulp和WebPacl的對比
- Task runner
Grunt處理SASS轉(zhuǎn)換成為CSS的過程
run('sass') source => **/**.sass => SASS **/*.css* => .tmp/
run('autoprefixer') .tmp/ => **/*.css* => Auto-prefixer => **/*.css* dest
對應(yīng)的Task Runner表示任務(wù)運(yùn)行器的意思!
但是對于對應(yīng)的Gulp來講的話,它是以流式的Task Runner起作用的!
source ==> **/*.sass ==> SASS ==> **/*.css ==> Auto-prefixer ==> **/*.css ==> desc
相較于Grunt來看的話,沒有對應(yīng)的temp文件夾,對應(yīng)的數(shù)據(jù)是存放在內(nèi)存中的,與此同時(shí)對應(yīng)的任務(wù)是 流式構(gòu)建
- 那么問題來了,對應(yīng)的Webpack的區(qū)別是什么?
在Webpack中對于資源的處理又是如何做的呢? 將對應(yīng)的資源都是當(dāng)成模塊來處理!
通過對應(yīng)的entry來索引模塊的依賴樹之后傳遞給Webpack的引擎,加載對應(yīng)的loader文件解析成對應(yīng)的CSS JS文件!
03|初級分析-使用Webpack內(nèi)置的stats
- stats:構(gòu)建的統(tǒng)計(jì)信息
- package.json中使用stats
"scripts":{
"build:stats":"webpack --env production --json > stats.json"
}
- Node API中使用
const wbepack = require("webpack");
const config = require("./webpack.config.js")("production");
webpack(config,(error,stats)=>{
if(error){
return console.error(error);
}
if(stats.hasError){
return console.error(stats.toString("errors-only"));
}
console.log(stats);
})
粒度太粗,看不出問題所在!
如果說是對應(yīng)的速度分析-使用speed-measure-webpack-plugin
- 代碼示例
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins:{
new MyPlugin(),
new MyOtherPlugin()
}
})
對應(yīng)的可以看到每個(gè)Loader和插件執(zhí)行耗時(shí)!
體積分析-使用webpack-bundle-analyzer
- 代碼示例:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins:{
new BundleAnalyzerPlugin()
}
}
構(gòu)建完成之后會在8888端口展示大小!
對應(yīng)的速度優(yōu)化策略
- 使用Webpack4
- 多進(jìn)程/多實(shí)例構(gòu)建
- 分包
- 緩存
- 縮小構(gòu)建目標(biāo)
- 使用新的Webpack帶來的受益確實(shí)不錯(cuò),那么使用Webpack4究竟好在哪里,為什么要使用Webpack4?
- V8帶來的優(yōu)化(for or替代forEach,Map和Set替代Object,includes替代indexOf)
- 默認(rèn)使用更快的md4 hash算法
- webpacks AST直接從loader傳遞給AST,減少了解析時(shí)間
- 使用字符串方法替代正則表達(dá)式
- 多進(jìn)程/多實(shí)例構(gòu)建-資源并行解析可選方案
thread-loader 可選方案
- paraller-webpack
- HappyPack
多進(jìn)程/多實(shí)例-使用HappyPack解析資源
原理:每次Webpack解析一個(gè)模塊,HappyPack會將它以及他的依賴分配給worker線程中
代碼示例:
export.plugins = {
new HappyPack({
id:"jsx",
threads:4,
loaders:['babel-loader']
}),
new HappyPack({
id:"styles",
threads:2,
loaders:['style-loader','css-loader','less-loader']
})
}
對應(yīng)的工作流程:
HappyThreadPool
^
|
|
HappyPlugin
^
|
|
HappyLoader
^
|
|
Webpack
中間部分:
HappyThread[1,N]
右邊部分:
HappyWorkerCHannel[1,N]
|
|
HappyWork[1,N]
|
|
webpack.loader
多進(jìn)程/多實(shí)例-并行壓縮
- 方法一:使用parallel-uglify-plugin插件
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin");
module.exports = {
plugins:[
new ParallelUglifyPlugin({
uglifyJS:{
output:{
beautify:false,
comments:false
},
compress:{
warnings:false,
drop_console:true,
collapse_vars:true,
reduce_vars:true
}
}
})
]
}
- 方法二:uglifyjs-webpack-plugin開啟parallel參數(shù)
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
plugins:[
new UglifyJsPlugin({
uglifyOptions:{
warnings:false,
parse:{},
compress:{},
mangle:true,
output:null,
toplevel:false,
nameCache:null,
ie8:false,
keep_fnames:false
},
parallel:true
})
]
}
分包-設(shè)置Externals
思路:將React,react-dom基礎(chǔ)包通過cdn引入,不打入bundle中
方法:使用html-webpack-externals-plugin
const HtmlWebpackExternalsPlugin = require("html-webpack-externals-plugin");
plugins:[
new HtmlWebpackExternalsPlugin({
externals:[
{
module:'react',
entry:'//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123',
global:'React'
},{
module:'react-dom',
entry:'//11.url.cn/now/lib/15.1.0/react-dom.min.js?_bid=3123',
global:'ReactDOM'
}
]
})
]
進(jìn)一步分包-預(yù)編譯資源模塊
- 思路:將React,react-dom,redux,react-redux基礎(chǔ)包和業(yè)務(wù)基礎(chǔ)打包成為一個(gè)文件
- 方法:使用DLLPlugin進(jìn)行分包,DllReferencePlugin對manifest.json引用
const path = require("path");
const webpack = require("webpack");
module.exports={
context:process.cwd(),
resolve:{
extensions:['.js','.jsx','.json','.less','.css'],
modules:[__dirname,'node_modules']
},entey:{
library:[
'react','react-dom','redux','react-redux'
]
},
output:{
filename:'[name].dll.js',
path:path.resolve(__dirname,'./build/library'),
library:'[name]'
},plugins:[
new webpack.DLLPlugin({
name:'[name]',
path:'./build/library/[name].json'
})
]
}
緩存
- 目的:提升二次構(gòu)建速度
- 方法:使用HardSourceWEbpackPlugin或者cache-loader
module.exports = {
plugins:new HardSourceWebpackPlugin({
cacheDirectory:'node_modules/.cache/hard-source/[confighash]',
configHash:function(webpackConfig){
return require('node-object-hash')({sort:false}).hash(webpackCOnfig);
},
environmentHash:{
root:process.cwd(),
directories:[],
files:['package-lock.json','yarn.lock']
},
info:{
mode:'none',level:"debug"
},cachePrune:{
maxAge:2*24*60*60*1000,
sizeThreshold:50*1024*1024
}
})
}
縮小構(gòu)建的目標(biāo):
- 目的:盡可能的少模塊構(gòu)建
- 比如:babel-loader不解析node_modules
module.exports = {
rules:{
text:/\.js$/,
loader:'happypack/loader',
exclude:'node_modules'
}
}
體積優(yōu)化策略:
- Scope Hoisting
- Tree-shaking
- 公共資源分離
- 圖片壓縮
- 動態(tài)Polyfill
- Scope-Hoisting原理
原理:將所有模塊的代碼按照引用順序放在一個(gè)函數(shù)作用域中,然后適當(dāng)?shù)闹孛恍┳兞糠乐棺兞棵麤_突
對比:通過scope hoisting可以減少函數(shù)聲明代表
示例代碼:
module.exports = {
plugins:[
new webpack.optimize.ModuleConcatenationPlugin()
]
}
- 對應(yīng)的要求必須是ES6的語法,CJS的方式不支持
Tree-shaking
- 概念:1個(gè)模塊可能有多個(gè)方法,只要其中的某個(gè)方法使用到了,則整個(gè)文件都會被打倒bundle里面去,Tree-shaking就是指把用到的方法打入bundle,沒用到的方法會在uglify階段被擦除掉!
- 使用:webpack默認(rèn)支持,在.babelrc里面設(shè)置modules:false即可
- 要求:必須是ES6的語法,CJS的方式不支持
公共資源分離
- 目的:提取多頁面公共JS chunk代碼
- 使用
- webpack3使用commonsChunkPlugin
- webpack4使用SplitChunksPlugin
module.exports = {
optimization:{
splitChunks:{
chunks:'async',
minSize:30000,
maxSize:0,
minChunks:1,
maxAsyncRequests:5,
maxInitialRequests:3,
automaticNameDelimiter:'~',
name:true,
cacheGroups:{
vendors:{
test:/[\\/\]node_modules[\\/]/,
priority:-10
},default:{
minChunks:2,
priority:-20,
reuseExistingChunk:true
}
}
}
}
}
圖片壓縮:
- 要求:基于Node庫的imagemin或者tinypng API
- 使用:配置image-webpack-loader
return {
test:/\.{png|svg|jpg|gif|blob}$/,
yse:[{
loader:'file-loader',
options:{
name:`${filename}img/[name]${hash}.[ext]`
}
},{
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
}
}
}]
}
構(gòu)建體積優(yōu)化-動態(tài)Polyfill
babel-polufill
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 是否采用 |
|---|---|---|---|
| babel-polyfill | 官方推薦 | 單獨(dú)構(gòu)建 react前加載 包體積比較大 | × |
| babel-plugin-transform-runtime | 只能用到類或者方法 對體積較小 | 不適用于復(fù)雜的開發(fā)環(huán)境 | × |
| map,set的polyfill | 定制化體積小 | 重復(fù)造輪子 體積小染有用的都加載 | × |
| polyfill-service | 只給用戶需要的 社區(qū)和維護(hù) | 部分國內(nèi)瀏覽器可能無法識別 但是可以降級返回所有的polyfill | √ |
如何動態(tài)使用Polyfill service?
- 使用polufill.io官方提供的服務(wù)
- 基于官方自建的polyfill的服務(wù)
總結(jié)和展望:
- 遇到打包速度和體積問題如何優(yōu)化
- 體積優(yōu)化的策略
- 速度優(yōu)化的策略
- 弄清楚基本原理比較重要