本章讲解是webpack打包时候能够进行的性能优化,让大型项目在打包的时候能够变得更快,这种打包运用在开发环境下,让编码打包变得快捷。这里分三块内容演说,【webpack构建速度(用于生产环境)】、【webpack构建速度(不用于生产环境)】、【webpack优化产出代码】、【共用的】
一、webpack构建速度(用于生产环境)
1.尽可能少的模块上使用Loader
比如说,我们在js打包的时候加上exclude:/node_modules/,这样子可以不打包node_modules里面的js文件;也可以使用include:path.resolve(__dirname, '../src')就是说只打包自己写的源代码文件。
具体代码如下:
rules: [{ test: /\.js$/, exclude: /node_modules/,//或 include:path.resolve(__dirname, '../src') use: [{loader: 'babel-loader'}, {loader: 'imports-loader?this=>window'}]
}]
node_modules的第三方模块都是编译过,不需要二次打包,合理使用exclude,include,像image,子体文件就没有必要了。
2.尽可能少的使用plugin,同时确保plugin的可靠性
不要选择一些冗余的没有意义的插件,要使用性能比较好,官方推荐过的插件。
比如下面就是推荐的,如:
- HtmlWebpackPlugin插件是为了能够分离说html代码文件
- MiniCssExtractPlugin是为了将css代码从js代码中分离出来,就节约了代码打包压缩的时间。
- OptimizeCSSAssetsPlugin是给css代码进行压缩的plugin
- CleanWebpackPlugin是为了能够在下次打包之前清理dist目录的问文件,防止文件叠加。
等等这些有意义的plugin。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
plugins: [new MiniCssExtractPlugin({filename: '[name].css'})
],
3、resolve参数合理配置
①不加后缀的文件引用
我们引入文件都是这样子的,都是在后面加入.js、.jsx、.vue等等。自从有了webpack之后,我们的引入方式不需要后缀了,只因为在webpack中多了extensions,代码如下;
resolve: {extensions: ['.js', '.jsx']
}
这时候,小伙伴又动起了脑袋瓜子了,那.css、.jpg、也像这样子引进来不添加后缀,可以吗?就像这样子:
resolve: {extensions: ['./css', './jpg', './js', '.jsx']
}
可以是可以,但是.css、.jpg这样子的东西在每次查找时候会损耗性能的。一般来说,像.js\.jsx\.vue\.json等等这样子的文件可以加,而像.css\.jpg等等,自己手动去配置就好。
②直接引用文件目录(一般来说是不推荐的)
想要import Child from './child/'这样通过目录的方式引入child.js文件,本身这样引入是不行的,如果把child.js改成index.js也可以,但是现在是child.js,不是index.js怎么弄,就是下面代码中的mainFiles这招。
resolve: {extensions: ['.js', '.jsx'],// 会优先寻找文件下的index文件找不到再去寻找child文件mainFiles: ['index', 'child']
}
这样额外配置resolve里面的内容也会有影响,一般我们不这样配。
③通过alias去将路径变成一个变量
在以前,我们引入一个文件是import parent from './../../../a/b/c/d/parent.js',就是说,我们要通过很多层才能找到要引入的文件的路径。但是自从webpack里面有了alias之后,我们就可以换一种路径写法了。
resolve: {alias: {// __dirname对应webpack打包目录child: path.resolve(__dirname, '../src/react/child/child.js')}},
然后像下面一样引用就可以了,import parent from 'child'就可以了
4.IgnorePlugin
避免引入无用模块,比如moment库全部引入会太大,怎么做呢?
new webpack.IgnorePlugin(/\.\/locale/, /moment/)// moment不要locale模块
然后在业务代码中引入对应的库模块就行。
原来的代码:
import moment from 'moment';
moment.locale('zh-cn');
console.log('locale', moment.locale());
console.log('data', moment().format('ll'))
构建大小:几乎把所有的moment都引进来了
更改后的代码:引入特定的库模块
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
console.log('locale', moment.locale());
console.log('data', moment().format('ll'))
你看,明显小了很多
5. noParse 避免重复打包
像有些.min的文件不需要重复打包就不要打包了。因为这种文件很长时间不会变化的。
避免重复打包,用法:举react的例子
module: {noParse: [/react\.min\.js/]
}
6.Scope Hosting
代码体积更小,创建函数作用域更少,代码可读性更好。这部分查看官网配置吧。
二、webpack构建速度(用于开发环境)
1.通过使用DllPlugin提高打包速度
这种打包作用于开发模式(development),一旦是生产模式(production)不生效了。
这个plugin意思是说,第二次打包直接使用缓存的。
① src/index.js里面放一些源代码,对lodash和jquery进行打包
import _ from 'lodash';
import $ from 'jquery';console.log(_.join(['this','is','app'], ' '))
console.log($('body').css('background', 'red'))
像lodash这样的第三方模块加进去后,打包时平均790mm像这种代码不会变多第三方模块,我们不必每次都去打包分析,第二次直接用第一次分析好多结果就好了。接下来就教你怎么做吧。
首先建立一个webpack.dll.js
② build/webpack.dll.js
const path = require('path');
module.exports = {mode: 'production',// 对这三个做存储entry:{vendors: ['jquery', 'lodash']},output:{filename: '[name].dll.js',// 打包后放入dll文件夹下path: path.resolve(__dirname, '../dll'),// 通过一个全局变量暴露出去,全局变量叫做vendorslibrary: '[name]'}
}
③ package.json
添加一个配置项
"scripts": {"build:dll": "webpack --config ./build/webpack.dll.js"
},
打包完成是这样子的
怎么引用vendors.dll.js,我们把vendors.dll.js植入到index.html的script标签里面,就可以全局引入vendors这个变量,很简单吧。
还没完,接着走。
④安装插件
插件说明:插件将给定的JS或CSS文件加入到Webpack 的公共文件中(如webpack.config.js),并通过html-webpack-plugin注入到生成的html中的列表中去,并将插件添加到配置中,为其提供一个文件路径。
npm install add-asset-html-webpack-plugin --save
⑤ 在webpack.common.js做配置
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
plugins: [// filepath表示要增加到文件是哪些new AddAssetHtmlWebpackPlugin({// 引入的就是已经打包好的文件dll目录下的文件filepath: path.resolve(__dirname, '../dll/vendors.dll.js')})
],
然后运行npm run dev ("webpack-dev-server --config ./build/webpack.common.js"),打开控制台,可以看到已经引入了vendors.dll.js。也有了vendors这个全局变量。这个会自动把东西引入到html的script中去。
目标越来越近,但是这打包好的东西怎么被原文件所使用呢?那我们就要做一些映射了。
⑥、映射,还是build/webpack.dll.js,把库里面一些第三方模块的映射关系放到了dll下面的vendors.mainfest.json
const path = require('path');
const webpack = require('webpack');
module.exports = {mode: 'production',// 对这三个做存储entry:{vendors: ['jquery', 'lodash']},output:{filename: '[name].dll.js',// 打包后放入dll文件夹下path: path.resolve(__dirname, '../dll'),// 通过一个全局变量暴露出去,全局变量叫做vendorslibrary: '[name]'},plugins: [// 用这个插件分析这个库,把库里面一些第三方模块的映射关系放到了dll下面的vendors.mainfest.jsonnew webpack.DllPlugin({name: '[name]',path: path.resolve(__dirname, '../dll/[name].manifest.json')})]
}
⑦ webpack.common.js 去找映射
module.exports = {plugins: [// filepath表示要增加到文件是哪些// 引入的就是已经打包好的文件dll目录下的文件new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/vendors.dll.js')}),/*** 使用这个插件,当打包index.js的时候,会引入一些第三方模块,当发现第三方模块的时候,会去manifest去找映射关系* 找到就不用再重新分析打包了,去vendors这个全局变量找就好了* 不在第三方模块,才会去分析*/new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')})],
}
那么整个配置就完毕了,这样子我们可以通过git.bash可以看到,打包的时间明显减少了。
还没过瘾接着来,我们可以对webpack.dll.js进行拆分,不要全都堆在vendors下面,比如lodash放在vendors下面,react的内容放在react下面,代码如下:
const path = require('path');
const webpack = require('webpack');module.exports = {mode: 'production',entry:{// 可以这样子拆分vendors: ['lodash'],jquery: ['jquery']},output:{filename: '[name].dll.js',path: path.resolve(__dirname, '../dll'),// 通过一个全局变量暴露出去,全局变量叫做vendorslibrary: '[name]'},plugins: [// 用这个插件分析这个库,把库里面一些第三方模块的映射关系放到了dll下面的vendors.mainfest.jsonnew webpack.DllPlugin({name: '[name]',path: path.resolve(__dirname, '../dll/[name].manifest.json')})]
}
再执行npm run build:dll("build:dll": "webpack --config ./build/webpack.dll.js")。生成了vendors.dll.js和react.dll.js两个文件。同时webpack.commmon.js也要增加新增的文件。就像这样子:
module.exports = {plugins: [new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/vendors.dll.js')}),new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/jquery.dll.js')}),new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')}),new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')})]
}
可是我们想啊,如果大项项目,拆分的越多,上面的东西也会越多,也就不断的加dll.js和mainfest.json。Binggo,我们做个优化吧,
优化:
把基础的plugins提取出来
const plugins = [// HtmlWebpackPlugin会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中new HtmlWebpackPlugin({template: 'src/index.html'}),new CleanWebpackPlugin()
]
接着通过node分析dll下有几个dll文件,有几个mainfest.json文件。然后动态的往plugins里面添加这个AddAssetHtmlWebpackPlugin和DllReferencePlugin。
代码如下:
// 引入node fs
const fs = require('fs');
// 读取dll文件的所有文件名
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
const plugins = [// HtmlWebpackPlugin会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中new HtmlWebpackPlugin({template: 'src/index.html'}),new CleanWebpackPlugin()
];// 读取dll文件的所有内容
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));files.forEach(file =>{// // filepath表示要增加到文件是哪些if(/.*\.dll.js/.test(file)) {plugins.push(new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll', file)}))}// /**// * 使用这个插件,当打包index.js的时候,会引入一些第三方模块,当发现第三方模块的时候,会去mainfest去找映射关系// * 找到就不用再重新分析打包了,去vendors这个全局变量找就好了// * 不在第三方模块,才会去分析// */if(/.*\.manifest.json/.test(file)){plugins.push(new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll', file)}))}
})// 本人改造
//function dll(exg, file){
// if(exg.test(file)){
// plugins.push(new AddAssetHtmlWebpackPlugin({
// filepath: path.resolve(__dirname, '../dll', file)
// }))
// }
// }
// files.forEach(file=>{
// dll(/.*\.dll.js/, file);
// dll(/.*\.manifest.json/, file);
// })
这会终于结束了。
2. thread-loader,parallel-webpack,happypack多进程打包
可以对多个页面进行一起打包。
happyPack多进程打包
因为js是单线程的,我们通过这个开启多进程打包来提高构建速度(特别是多核CPU)。
但是,打包时间会长点吧,所以:
(1)项目较大,打包较慢,开启多进程能提高速度
(2)项目较小,打包很快,开启多进程会降低速度(进程开销)
(3)按需使用
用法:先安装再使用
new HappyPack({id: 'babel',loaders: ['babel-loader?cacheDirectory']//以babel-loader为例而已
})
3.开发环境内存编译(也就是自动刷新【开发模式下】)
通过webpack-dev-server,把内容打包到内存中,提高打包速度,而不是打包生成dist,因为这样子毕竟是慢的。
又优点就会有缺点,说下它的缺点吧:
(1)自动刷新:整个网页全部刷新。速度较慢
(2)自动刷新:整个网页全部刷新,状态会丢失
4.热更新
热更新: 新代码生效,网页不刷新,状态不丢失,填写的数据也不会丢失。
用法:
new webpack.HotModuleReplacementPlugin()
然后在devserver设置hot: true,开启热更新。
然后更改样式时候不会更改结构,但是让js也有热更新需要有这段代码:
if (module.hot) {module.hot.accept('./print.js', function() {console.log('Accepting the updated printMe module!');printMe();})
}
上面代码意思是:监听print.js模块,只有更改print.js里面的东西才会触发热更新,但是如果是更改print.js以外的东西是不会触发热更新的。
模块的热替换相对来说比较难掌握,很容以一不小心就犯错导致失效,好在存在很多 loader,使得模块热替换的过程变得更容易。建议:如果自动刷新对开发没有什么影响就用自动刷新,如果有影响就用热更新,因为热更新必定需要成本的。
5.开发环境剔除无用的插件
在开发环境中的插件,你不要像线上打包环境一样去使用它,这会影响打包速度。
三、webpack优化产出代码
1.小图片base64编码
这里主要是通过file-loader与url-loader的配合去使用,如果图片较小则直接打包进js里面,这样子可以减少请求次数,如果图片过大可以单独的放入images包里面。
2.bundle加hash
bundle是产出的文件,当内容变了才变化hash,如果没变还是用原先的hash,就是命中缓存。
3.懒加载import语法
就是给模块加上import进行异步加载,懒加载可以看我的其他文章。
4.提取公共代码
这个就是将生产代码和开发代码分开,然后相同的代码放入webpack.common.js里面,生产代码放入webpack.prod.js里面,开发代码放入webpack.dev.js里面。就像下面这样子:
5.使用CDN加速
加上publicPath,就是在所有静态文件前面加上前缀,这样可以找到最近的服务器地址资源,增快加载速度。
6.使用production
- 会自动压缩代码,
- vue react等会自动删除调试代码(如开发环境的warning),
- 启动Tree-Shaking,有些代码不需要的就摇晃掉,ES module生效(静态加载),commomjs不生效(动态加载),我们写代码时候经常引入不必要的模块,而且还用不到,所以平时我们可以用Tree Shaking。
附加:Tree Shaking的解释说明
Tree Shaking 用来去掉没有引用的import内容,只支持ES Module的引入方式,默认是js文件的引用,其他引用会被去掉,需要通过sideEffects去设置。
配置方式:
mode为development时候,默认不带tree shakiing的,可以通过添加optimization: {usedExports: true}的配置项来加上,同时需要在package.json里面添加"sideEffects": false配置项,sideEffects的值还可以为数组。比如["*.css", babel]等等,可以对这些文件不做去掉处理,可以照常引用。
mode为production时候,默认会启动Tree Shaking的,然而sideEffects还是要配置的。可以减少打包的体积。
附加:ES Module和Commonjs区别
- ES6 Module静态引用,编译时引用
- Commonjs动态引用,执行时引用
- 只有ES6 Module 才能静态分析,实现Tree-Shaking
四、共用的:
1.更上技术的迭代(Node\Npm\Yarn)
因为技术的更新,很大程度上化优化打包的速度,官网会对新的版本进行优化,从而提高打包的速度。
2.合理使用sourceMap
sourceMap越详细打包越慢,所以我们用到最合适的sourceMap去影响打包速度。
devtool: source-map 生成map文件
- devtool: inline-source-map 不会生成map文,会提示到列
- devtool: cheap: 不会提示到列,只会提示业务代码,不提示第三方插件的代码
- devtool: module: 会提示第三方插件,如loader,plugin
- devtool: eval: 打包最快,但是提示比较少
建议:
production环境下使用:devtool: 'cheap-module-source-map'
development环境下使用:devtool: 'cheap-module-eval-source-map'
3.结合stats分析打包结果
通过打包分析,查看哪些耗时比较多,然后对包进行优化,这个部分参考
webpack4.x 系列(二) 之 懒加载 、Preload、Prefetch的讲解