文章目录
关于webpack的性能优化,主要体现在三个方面:
- 构建性能:是指在开发阶段的构建性能。当构建性能越高,开发效率越高。
- 传输性能:在这方面重点考虑网络中的总传输量、JS文件数量以及浏览器缓存。
- 运行性能:主要是指JS代码在浏览器端运行的速度。
构建性能
减少模块解析
模块解析包括:AST抽象语法树分析、依赖分析、模板语法替换
对某个模块不进行解析,可以缩短构建时间
如果某个模块不做解析,该模块经过loader处理侯的代码就是最终代码。
如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。
对于模块中没有其他依赖模块,则不需要解析,可以通过配置
module.noParse
进行处理:
module.exports ={mode:"development",module:{noParse:/JQuery/}}
优化loader性能
限制loader的应用范围
针对一些第三方库,不使用loader进行处理。例如
babel-loader
,转换一些本身就是用ES5语法书写的第三方库,反而会浪费构建时间。
因此通过
module.rules.exclude
或
module.rules.include
,排除或仅包含需要应用loader的场景。
module.exports ={module:{rules:[{test:/\.js$/,exclude:/node_modules/,//或// include: /src/,use:"babel-loader"}]}}
缓存loader结果
如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变,所以我们可以把loader的解析结果保存下来,让后续的解析直接用缓存的结果,具体实现如下:
module.exports ={module:{rules:[{test:/\.js$/,use:[{loader:"cache-loader",options:{cacheDirectory:"./cache"}},...loaders]},],},};
cache-loader的原理是,在执行loader之前,如果发现有缓存文件,则直接在loader.pitch函数里return源代码。
那么问题来了,loader明明不是从后往前执行的吗?那为什么cache-loader还可以拿到loader的缓存结果?
其实每个loader的运行过程中,还包括一个过程,即
pitch
functionloader(source){return`new source`}
loader.pitch=function(filePath){// 可返回可不返回// 如果返回,返回源代码}
module.exports = loader;
第一次打包时,会先把
filepath
交给
loader1.pitch
执行, 检查是否有缓存结果,若无缓存,往后执行。调用
loader2.pitch
,检查是否有缓存,若无缓冲,往后执行,依次类推…直到最后结束了再调用loader,当调用
cache-loader
时,就会返回loader处理的结果并缓存。
当第二次打包时(流程同上),若发现有缓存,则直接返回缓存结果,不会继续往后走了,示例图如下:
pitch的好处,可以根据是否有返回,来控制下一步到哪。
对于babel-loader,使用它本身的配置也是可以缓存的。
开启多线程打包
通过
thread-loader
会开启一个线程池,它会把后续的loader放到线程池的线程中运行,以提高构建效率。
module.exports ={module:{rules:[{test:/\.js$/,use:["thread-loader","babel-loader"]}]}};
放到线程池的loader的缺点:
- 无法使用 webpack api 生成文件。
- 无法使用自定义的 plugin api。
- 无法访问 webpack options。
thread-loader可以通过测试决定放置的位置。
由于开启和管理线程需要消耗时间,所以在小项目使用会增加构建时间。
热替换 (Hot Module Replacement)
其实,热更新是不能降低构建时间(可能还会稍微增加),因为它发生代码运行期间,但它可以降低代码改动到效果显现的时间。
//配置文件
module.exports ={devServer:{open:true,hot:true//开启HMR},plugins:[newHTMLWebpackPlugin({template:"./public/index.html"})]}//index.jsif(module.hot){// 是否开启热更新
module.hot.accept()// 接收热更新}
默认情况下,
webpack-dev-server
不管是否开启了热更新,当重新打包后,都会调用
location.reload
刷新页面
但如果运行
module.hot.accept()
,将改变这一行为,
module.hot.accept()
的作用是让
webpack-dev-server
通过
socket
管道,把服务器更新的内容发送到浏览器,然后,将结果交给插件
HotModuleReplacementPlugin
注入的代码执行插件
HotModuleReplacementPlugin
会根据覆盖原始代码,然后让代码重新执行。
对于样式热替换,可使用style-loader。
module.exports ={devServer:{open:true,hot:true//开启HMR},module:{rules:[{test:/\.css$/,use:["style-loader","css-loader"]}]},plugins:[newHTMLWebpackPlugin({template:"./public/index.html"})]}
为什么不使用
mini-css-extract-plugin
插件?
因为热更新发生时,
HotModuleReplacementPlugin
只会简单的重新运行模块代码。因此
style-loader
的代码一运行,就会重新设置style元素中的样式,而
mini-css-extract-plugin
生成文件是在构建期间,运行期间无法改动文件。
整个原理流程:
当开启热更新后,Webpack 会轮询问有没有哪些模块发生变化,如果文件内容发生改变,会异步下载更新的代码,向服务器发送请求。下载完毕后,服务器就会主动发送信息给浏览器告知有文件内容发生改变,浏览器发送请求给服务器请求发送修改后的资源文件,服务器接收到请求后把修改的资源文件发送给浏览器,浏览器把接收到的结果交给
HotModuleReplacementPlugin
,
HotModuleReplacementPlugin
再覆盖原始代码,再重新执行代码。
传输性能
分包
webpack默认情况下是不会分包的,它会把所有依赖文件合并到一个bundle中。而分包的时机是,当公共模块体积较大 或 有较少的变动,特别是在多页面打包的情况下,会存在多个chunk引入公共模块导致冗余代码的情况,占用打包体积。
分包的目的是在不影响源代码编写的情况下,减少公共代码,降低总体积(特别是一些大型的第三方库)和充分利用浏览器缓存。并非所有的情况都适合分包,需要视具体情况而定。
手动分包
总体思路:
- 先单独的打包公共模块,并利用
DllPlugin
生成资源清单。 - 手动引入公共模块,重新设置
clean-webpack-plugin
,然后使用DllReferencePlugin
控制打包结果。
具体打包过程如下:
- 开启
output.library
暴露公共模块 - 用
DllPlugin
创建资源清单 - 用
DllReferencePlugin
使用资源清单
// webpack.dll.config.jsconst webpack =require('webpack');
module.exports ={mode:"production",entry:{jquery:["jquery"],lodash:["lodash"]},output:{filename:"dll/[name].js",library:"[name]",//libraryTarget: "var" //暴露方式},plugins:[newwebpack.DllPlugin({path: path.resolve(__dirname,"dll","[name].manifest.json"),//资源清单的保存位置name:"[name]"//资源清单中,暴露的变量名})]};//webpack.config.js
module.exports ={plugins:[//指定资源清单,在打包时对照资源清单,当发现该模块是资源清单里的资源时不进行打包处理。newwebpack.DllReferencePlugin({manifest:require("./dll/jquery.manifest.json")}),newwebpack.DllReferencePlugin({manifest:require("./dll/lodash.manifest.json")})]};
引用:https://webpack.docschina.org/plugins/dll-plugin#dllplugin
自动分包
原理:
- 检查每个chunk编译的结果
- 根据分包策略,找到那些满足策略的模块
- 根据分包策略,生成新的chunk打包这些模块(代码有所变化)
- 把打包出去的模块从原始包中移除,并修正原始包代码
在代码层面,有以下变动
- 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码
- 原始包的代码中,使用数组中的公共代码
webpack提供了
optimization
配置项,其中
splitChunks
是分包策略的配置。
实际上,webpack在内部是使用
SplitChunksPlugin
进行分包的,分包时,webpack开启了一个新的chunk,对分离的模块进行打包。打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新chunk的产物
过去有一个库
CommonsChunkPlugin
也可以实现分包,不过由于该库某些地方并不完善,到了
webpack4
之后,已被
SplitChunksPlugin
取代。
一般分包是在生产环境下进行的。
分包策略有其默认的配置,只需小小改动即可应用大部分分包场景。
- chunks:用于配置需要应用分包策略的chunk。一般只需要配置为 all 即可- all: 对于所有的chunk都要应用分包策略- async:【默认】仅针对异步chunk应用分包策略- initial:仅针对普通chunk应用分包策略
- maxSize:可以控制包的最大字节数。如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包。 分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积
全局策略:
module.exports ={mode:"production",entry:{},output:{},optimization:{splitChunks:{// 分包策略chunks:"all",maxSize:60000,// 分包策略的其他配置automaticNameDelimiter:".",// 新chunk名称的分隔符,默认值~minChunks:1,// 一个模块被多少个chunk使用时,才会进行分包,默认值1minSize:30000,// 当分包达到多少字节后才允许被真正的拆分,默认值30000},},plugins:[],}
缓存组策略:
每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包。
默认情况下,webpack提供了两个缓存组:
module.exports ={optimization:{splitChunks:{//全局配置cacheGroups:{// 属性名是缓存组名称,会影响到分包的chunk名// 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置vendors:{test:/[\\/]node_modules[\\/]/,// 当匹配到相应模块时,将这些模块进行单独打包priority:-10// 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0},default:{minChunks:2,// 覆盖全局配置,将最小chunk引用数改为2priority:-20,// 优先级reuseExistingChunk:true// 重用已经被分离出去的chunk}}}}}
单模块体积压缩
代码压缩
代码压缩除了减少代码体积,还可以破坏代码的可读性,提升破解成本。
Terser
是一个新起的代码压缩工具,支持
ES6+
语法。
webpack
会内置
Terser
,当启用生产环境后即可用其进行代码压缩。
Terser官网:https://terser.org/
如果想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置即可:
module.exports ={optimization:{minimize:true,// 是否要启用压缩,默认情况下,生产环境会自动开启minimizer:[// 压缩时使用的插件,可以有多个newTerserPlugin(),// js压缩插件newOptimizeCSSAssetsPlugin()// css压缩插件],}}
terser、webpack、rollup.js都能够识别
/*#__PURE__ */
注释标记,
/*#__PURE__* /
的作用就是告诉打包工具该函数的调用不会产生副作用。
tree shaking
tree shaking可以移除模块之间的无效代码
如果运行环境是生产环境,tree shaking自动开启。
在编写代码时,由于tree shaking需要满足一定的代码规范,所以应该尽量注意规范。例如:
//使用export xxx;//导出import{xxx}from"xxx";// 导入//不使用exportdefault{xxx};//导出import xxx from"xxx";// 导入
当webpack依赖分析完毕后,
webpack
会根据每个模块每个导出是否被使用,标记其他导出为
dead code
,然后交给代码压缩工具处理,代码压缩工具最终移除掉那些
dead code
代码。
commonjs很难做到tree shaking,所以主流的库为了做tree shaking,都会发布其es6版本,比如lodash-es。
webpack在
tree shaking
的使用,有一个原则:一定要保证代码正确运行
在满足该原则的基础上,再来决定如何
tree shaking
因此,当
webpack
无法确定某个模块是否有副作用时,它往往将其视为有副作用
因此,某些情况可能并不是我们所想要的
//common.js
var n = Math.random();
//index.js
import "./common.js"
虽然我们根本没用有
common.js
的导出,但
webpack
担心
common.js
有副作用,如果去掉会影响某些功能
如果要解决该问题,就需要标记该文件是没有副作用的
在
package.json
中加入
sideEffects
{
"sideEffects": false
}
有两种配置方式:
- false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些css文件的导入
- 数组:设置哪些文件拥有副作用,例如:
["!src/common.js"]
,表示只要不是src/common.js
的文件,都有副作用
这种方式我们一般不处理,通常是一些第三方库在它们自己的
package.json
中标注
由于webpack无法对css完成tree shaking,所以可以通过正则匹配页面样式有没有引用进行移除样式代码。
可以通过
purgecss-webpack-plugin
进行处理,该插件对css module无法处理。
懒加载
通过动态导入模块,例如在判断里使用导入语句。
导入语句不能使用commonjs,虽然require支持动态导入,但是它在打包环节也会进入依赖分析。
动态加载可以使用import(),import作为es6的草案,webpack打包发现使用import()的调用,会对其单独打包,打包结果该代码时,浏览器会使用JSOP的方式远程去读取一个js模块,import()返回的是一个promise。
asyncfunctionrun(){if(判断条件){const{ chunk }=awaitimport(/* webpackChunkName:'自定义chunkName' */'xxx.js')}}run();
请求的异步的模块会加入webpackJsonp数组里。
值得注意的是,这样的异步导入是不可以做到tree shaking的,不过可以使用取巧的方法,通过一个媒介引入,打包分析过程既能tree shaking又能异步加载。
//媒介文件export{ xxx }from'目标文件'//主文件 asyncfunctionrun(){if(判断条件){const{ chunk }=awaitimport('媒介文件')}}run()
gzip
gzip是一种压缩文件的算法
gizp工作原理:
浏览器发送请求时,会在请求头中设置
Accept-Encoding:gzip,deflate,br
。表明浏览器支持gzip。服务器收到浏览器发送的请求之后,判断浏览器是否支持gizp,如果支持gzip,则向浏览器传送压缩过的内容,不支持则向浏览器发送未经压缩的内容。一般情况下,浏览器和服务器都支持gzip,响应头返回包含content-encoding:gzip。浏览器接收到服务器的响应之后判断内容是否被压缩,如果被压缩则解压缩显示页面内容。
对哪些文件压缩,采用哪种压缩算法,这个需要测试权衡,毕竟压缩文件和解压文件都是需要时间的,对于相对大点的文件一般会有收益。
webpack压缩参与的步骤在于将文件预压缩,当请求到来时直接响应已经压缩的文件,而不需要先压缩再响应。
使用
compression-webpack-plugin
插件对打包结果进行预压缩,可以移除服务器的压缩时间。
const CmpressionWebpackPlugin =require("compression-webpack-plugin")
module.exports ={plugins:[newCmpressionWebpackPlugin({// filename: "[file].gzip"test:/\.js/,//针对需要预压缩的文件minRatio:0.5//压缩比率})]};
以gzip为例,打包之后的文件包含了.js和.js.gz文件。
辅助工具
- ESlint:检查JS代码规范
- bundle-analyzer:生成代码分析报告,帮助提升代码质量和网站性能
运行性能
运行性能是指,JS代码在浏览器端的运行速度,它主要取决于我们书写代码质量的高低。
关于高性能的代码,可以参考常见的设计模式、代码规范、最佳实践等。
版权归原作者 是加薪呀 所有, 如有侵权,请联系我们删除。