Webpack学习笔记(优化篇)
webpack构建速度和体积优化策略
初级分析:使用webpack内置的stats
stats:构建的统计信息
package.json中使用stats
"scripts":{
"build:stats":"webpack --env production --json > stats.json"
...
}
Node.js中使用stats
const webpack =require("webpack");
const config =require("./webpack.config.js")("production");
webpack(config,(err, stats)=>{
if(err){
return console.error(err);
}
if(stats.hasErrors()){
return console.error(stats.toString("errors-only"));
}
console.log(Stats);
})
stats统计的缺陷:颗粒度比较粗,看不出问题所在
速度分析:使用speed-measure-webpack-plugin
使用步骤
安装
npm install --save-dev speed-measure-webpack-plugin
代码示例
constSpeedMeasurePlugin=require("speed-measure-webpack-plugin");
const smp =newSpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins:[
newMyPlugin(),
newMyOtherPlugin()
]
})
优点:可以看到每个loader和插件执行耗时
速度分析插件作用
分析整个打包总耗时
每个插件和loader的耗时情况
体积分析:使用webpack-bundle-analyzer
webpack-bundle-analyzer分析体积
代码示例:
constBundleAnalyzerPlugin=require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports ={
plugins:[
newBundleAnalyzerPlugin();
]
}
构建完成后会在8888端口展示构建资源大小
可以分析哪些问题?
•依赖的第三方模块文件大小•业务里面的组件代码大小
速度提升:使用高版本的webpack和Node.js
•构建时间降低了60%-98%
使用webpack4:优化原因
•V8带来的优化(for of 替代forEach、Map和Set替代Object,includes替代indexOf)•默认使用更快的md4 hash算法•webpacks AST可以直接从loader传递给AST,减少解析时间•使用字符串方法替代正则表达式
速度提升:多进程/多实例构建
多进程/多实例构建:资源并行解析可选方案
•thread-loader•parallel-webpack•HappyPack
多进程/多实例构建:使用HappyPack解析资源
原理:每次webpack解析一个模块,HappyPack会将它及它的依赖分配给worker线程中
代码示例:
exports.plugins =[
newHappyPack({
id:'jsx',
threads:4,
loaders:['babel-loader']
}),
newHappyPack({
id:'styles',
threads:2,
loaders:['style-loader','css-loader','less-loader']
})
]
多进程/多实例构建:使用thread-loader解析资源
原理:每次webpack解析一个模块,thread-loader会将它及它的依赖分配给worker线程中
module.exports = smp.wrap({
entry: entry,
output:{
path: path.join(__dirname,'dist'),
filename:'[name]_[chunkhash:8].js'
},
mode:'production',
module:{
rules:[
{
test:/.js$/,
use:[
{
loader:'thread-loader',
options:{
workers:3
}
},
'babel-loader',
]
}
]
}
})
对进程/多实例:并行压缩
方法一:使用parallel-uglify-plugin插件
constParallelUglifyPlugin=require('webpack-parallel-uglify-plugin');
module.exports ={
plugins:[
newParallelUglifyPlugin({
uglifyJS:{
output:{
beautify:false,
comments:false
},
compress:{
warnings:false,
drop_console:true,
collapse_vars:true,
reduce_vars:true,
}
}
})
]
}
方法二:uglifyjs-webpack-plugin开启parallel参数
constUglifyJsPlugin=require('uglifyjs-webpack-plugin');
module.exports ={
plugins:[
newUglifyJsPlugin({
uglifyOptions:{
warnings:false,
parse:{},
compress:{},
mangle:true,
output:null,
toplevel:false,
nameCache:null,
ie8:false,
keep_fnames:false
},
parallel:true
})
]
}
方法三:terser-webpack-plugin开启parallel参数
constTerserPlugin=require('terser-webpack-plugin');
module.exports ={
optimization:{
minimizer:[
newTerserPlugin({
paralles:4
})
]
}
}
进一步分包:预编译资源模块
分包:设置Externals
思路:将react、react-dom基础包通过cdn引入,不打入bundle中
方法:使用html-webpack-externals-plugin
constHtmlWebpackExternalsPlugin=require('html-webpack-externals-plugin');
module.exports ={
...
plugins:[
newHtmlWebpackExternalsPlugin({
externals:[
{
module:'react',
entry:'https://11.url.cn/now/lib/16.2.0/react.min.js',
global:'React',
},
{
module:'react-dom',
entry:'https://11.url.cn/now/lib/16.2.0/react-dom.min.js',
global:'ReactDOM',
}
]
})
]
...
}
分包:预编译资源模块
思路:将react、react-dom、redux、react-redux基础包和业务基础包打包成一个文件
方法:使用DLLPLugin进行分包,DllReferencePlugin对manifest.json引用
使用DLLPlugin进行分包
const path =require('path');
const webpack =require('webpack');
module.exports ={
context: process.cwd(),
resolve:{
extensions:['.js','.jsx','.json','.less','.css'],
modules:[__dirname,'node_modules']
},
entry:{
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'
})
]
}
使用DllReferencePlugin引用manifest.json
在webpack.config.js引入
module.exports ={
plugins:[
new webpack.DllReferencePlugin({
manifest:require('./build/library/manifest.json')
})
]
}
速度提升:充分利用缓存提升二次构建速度
缓存目的:提升二次构建速度
缓存思路:
•babel-loader开启缓存•terser-webpack-plugin开启缓存•使用cache-loader或者hard-source-webpack-plugin
速度提升:缩小构建目标
目的:尽可能的少构建模块
比如babel-loader不解析node_modules
减少文件搜索范围
•优化resolve.modules配置(减少模块搜索层级)•优化resolve.mainFields配置•优化resolve.extensions配置•合理使用alis
module.exports ={
resolve:{
alias:{
react: path.resolve(__dirname,'./node_modules/react/dist/react.min.js'),
},
modules:[path.resolve(__dirname,'node_modules')],
extensions:['.js'],
mainFields:['main'],
}
}
使用Tree Shaking擦除无用的JavaScript和Css
tree shaking(摇树优化)复习
概念:1个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉。
使用:webpack默认支持,在.babelrc里设置modules: false即可
•production mode的情况下默认开启
要求:必须是ES6语法,CJS的方式不支持
无用的CSS如何删除掉?
PurifyCSS:遍历代码,识别已经用到的CSS class
uncss:HTML需要通过jsdom加载,所有的样式通过PostCSS解析,通过document.querySelector来识别在html文件里面不存在的选择器
在webpack中如何使用PurifyCSS?
使用purgecss-webpack-plugin
•https://github.com/FullHuman/purgecss-webpack-plugin
和mini-css-extract-plugin配合使用
const path =require('path');
const glob =require('glob');
constMiniCssExtractPlugin=require('mini-css-extract-plugin');
constPurgecssPlugin=require('purgecss-webpack-plugin');
const PATHS ={
src: path.join(__dirname,'src')
}
module.exports ={
module:{
rules:[
{
test:/\.css/,
use:[
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins:[
newMiniCssExtractPlugin({
filename:'[name].css',
}),
newPurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`,{nodir:true})
})
]
}
使用webpack进行图片压缩
要求:基于Node库的imagemin或者tinypngAPI
使用:配置image-webpack-loader
return{
test:/\.(png|svg|jpg|gif|blob)$/,
use:[{
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
}
}
}]
}
Imagemin的优点分析
•有很多定制选项•可以引入更多第三方优化插件,例如pngquant•可以处理多种图片格式
Imagemin的压缩原理
•pngquant:是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG文件小60-80%)的更高效的8位PNG格式,可显著减小文件大小。•pngcrush:其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNG IDAT数据流的大小。•optipng:其设计灵感来自于pngcrush。optipng可将图像文件重新压缩为更小尺寸,而不会丢失任何信息。•tinypng:也是将24位png文件转化为更小有索引的8位图片,同时所有非必要的metadata也会被剥离掉。
使用动态Polyfill服务
babel-polyfill:打包后体积88k,占比较大
构建体积优化:动态Polyfill
方案 | 优点 | 缺点 | 是否采用 |
babel-polyfill | React16官方推荐 | 1、包体积200K+,难以单独抽离Map、Set;2、项目里react是单独引用的cdn,如果要用它,需要单独构建一份放在react前加载 | 否 |
babel-plugin-transform-runtime | 能只polyfill用到的类或方法,相对体积较小 | 不能polyfill原型上的方法,不适用于业务项目的复杂开发环境 | 否 |
自己写Map、Set的polyfill | 定制化高,体积小 | 1、重复造轮子,容易在日后年久失修成为坑;2、即使体积小,依然所有用户都要加载 | 否 |
polyfill-service | 只给用户返回需要的polyfill,社区维护 | 部分国内奇葩浏览器UA可能无法识别(但可以降级返回所需全部polyfill) | 是 |
Polyfill Service原理
识别User Agent,下发不同的Polyfill
如何使用动态Polyfill Service
1、polyfill.io 官方提供的服务
2、基于官方自建polyfill服务
//huayang.qq.com/polyfill_service/v2/polyfill.min.js?unknown=polyfill&features=Promise,Map,Set