今天为了更好地了解一下Webpack打包优化的一些内容,看了一下NEXT公开课,Webpack打包极限优化,感兴趣的朋友可以去腾讯课堂看看,我这里也是对于公开课的笔记总结!
其中讲到的点如下所示:
-
WebPack基础知识
-
构建速度优化
-
案例优化效果对比
-
如何分析页面打包问题?
-
构建体积优化
-
总结和展望
01|为什么需要构建工具?
- 转换ES6语法
- 转换JSX
- CSS前缀补全/预处理器
- 压缩混淆
- 图片压缩
其中对应的浏览器的支持情况,涉及到了 CANIUSE中的兼容处理!
我们可以通过最基本的例子来演示:
- WebPack脚本
const path = require("path");
module.exports = {
mode:'production',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'dist'),
filename:'bundle.js'
}
}
- 构建结果
<!DOCTYPE html>
<html>
<head>
<title>example</title>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>
其实通过Webpack的脚本很好看出具体做了什么?
mode就是制定脚本的运行环境,对应的entry就是表示的入口文件,output则是指定编译的目录和文件!
执行流程
Entry Output
app/index.js Webpack/Plugins/Loaders/ dist/app.js
| 对应的loader进行处理 |
| |
| Split dist/0.js
app/component.js
|
|
app/util.js
对应的定位就是模块打包器!
Entry对应的入口,根据对应的entry就可以知道对应的依赖树,比如说 index.js依赖 component.js component.js依赖util.js
对应的Plugin和Loader有什么区别吗?
- Loader主要是做资源的解析操作
- Plugin更加强大,为Webpack的拓展,可以做很多loader没法做的能够增强Webpack的功能!
目标代码:
- dist文件中
- 为了更好的加载会将文件做分割以便更好地做懒加载!
02|Grunt,Gulp和WebPacl的对比
- Task runner
Grunt处理SASS转换成为CSS的过程
run('sass') source
=> **/**.sass
=> SASS **/*.css*
=> .tmp/
run('autoprefixer') .tmp/
=> **/*.css*
=> Auto-prefixer => **/*.css*
dest
对应的Task Runner表示任务运行器的意思!
但是对于对应的Gulp来讲的话,它是以流式的Task Runner起作用的!
source ==> **/*.sass
==> SASS ==> **/*.css
==> Auto-prefixer ==> **/*.css
==> desc
相较于Grunt来看的话,没有对应的temp文件夹,对应的数据是存放在内存中的,与此同时对应的任务是 流式构建
- 那么问题来了,对应的Webpack的区别是什么?
在Webpack中对于资源的处理又是如何做的呢? 将对应的资源都是当成模块来处理!
通过对应的entry来索引模块的依赖树之后传递给Webpack的引擎,加载对应的loader文件解析成对应的CSS JS文件!
03|初级分析-使用Webpack内置的stats
- stats:构建的统计信息
- 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);
})
粒度太粗,看不出问题所在!
如果说是对应的速度分析-使用speed-measure-webpack-plugin
- 代码示例
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins:{
new MyPlugin(),
new MyOtherPlugin()
}
})
对应的可以看到每个Loader和插件执行耗时!
体积分析-使用webpack-bundle-analyzer
- 代码示例:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins:{
new BundleAnalyzerPlugin()
}
}
构建完成之后会在8888端口展示大小!
对应的速度优化策略
- 使用Webpack4
- 多进程/多实例构建
- 分包
- 缓存
- 缩小构建目标
- 使用新的Webpack带来的受益确实不错,那么使用Webpack4究竟好在哪里,为什么要使用Webpack4?
- V8带来的优化(for or替代forEach,Map和Set替代Object,includes替代indexOf)
- 默认使用更快的md4 hash算法
- webpacks AST直接从loader传递给AST,减少了解析时间
- 使用字符串方法替代正则表达式
- 多进程/多实例构建-资源并行解析可选方案
thread-loader 可选方案
- paraller-webpack
- HappyPack
多进程/多实例-使用HappyPack解析资源
-
原理:每次Webpack解析一个模块,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']
})
}
对应的工作流程:
HappyThreadPool
^
|
|
HappyPlugin
^
|
|
HappyLoader
^
|
|
Webpack
中间部分:
HappyThread[1,N]
右边部分:
HappyWorkerCHannel[1,N]
|
|
HappyWork[1,N]
|
|
webpack.loader
多进程/多实例-并行压缩
- 方法一:使用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参数
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
})
]
}
分包-设置Externals
-
思路:将React,react-dom基础包通过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'
}
]
})
]
进一步分包-预编译资源模块
- 思路:将React,react-dom,redux,react-redux基础包和业务基础打包成为一个文件
- 方法:使用DLLPlugin进行分包,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'
})
]
}
缓存
- 目的:提升二次构建速度
- 方法:使用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
}
})
}
缩小构建的目标:
- 目的:尽可能的少模块构建
- 比如:babel-loader不解析node_modules
module.exports = {
rules:{
text:/\.js$/,
loader:'happypack/loader',
exclude:'node_modules'
}
}
体积优化策略:
- Scope Hoisting
- Tree-shaking
- 公共资源分离
- 图片压缩
- 动态Polyfill
- Scope-Hoisting原理
原理:将所有模块的代码按照引用顺序放在一个函数作用域中,然后适当的重命名一些变量防止变量名冲突
对比:通过scope hoisting可以减少函数声明代表
示例代码:
module.exports = {
plugins:[
new webpack.optimize.ModuleConcatenationPlugin()
]
}
- 对应的要求必须是ES6的语法,CJS的方式不支持
Tree-shaking
- 概念:1个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打倒bundle里面去,Tree-shaking就是指把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉!
- 使用:webpack默认支持,在.babelrc里面设置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
}
}
}]
}
构建体积优化-动态Polyfill
babel-polufill
方案 | 优点 | 缺点 | 是否采用 |
---|---|---|---|
babel-polyfill | 官方推荐 | 单独构建 react前加载 包体积比较大 | × |
babel-plugin-transform-runtime | 只能用到类或者方法 对体积较小 | 不适用于复杂的开发环境 | × |
map,set的polyfill | 定制化体积小 | 重复造轮子 体积小染有用的都加载 | × |
polyfill-service | 只给用户需要的 社区和维护 | 部分国内浏览器可能无法识别 但是可以降级返回所有的polyfill | √ |
如何动态使用Polyfill service?
- 使用polufill.io官方提供的服务
- 基于官方自建的polyfill的服务
总结和展望:
- 遇到打包速度和体积问题如何优化
- 体积优化的策略
- 速度优化的策略
- 弄清楚基本原理比较重要
网友评论