当前位置: 代码迷 >> 综合 >> webpack4.x 系列(四) ? webpack打包性能优化
  详细解决方案

webpack4.x 系列(四) ? webpack打包性能优化

热度:87   发布时间:2023-12-23 14:14:42.0

本章讲解是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区别

  1. ES6 Module静态引用,编译时引用
  2. Commonjs动态引用,执行时引用
  3. 只有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的讲解