vue前端性能优化有哪些方法,vue性能调优
本文主要针对Vue.js 2.x版本。毕竟Vue.js 2.x在未来一段时间内还是我们工作中的主流版本。对vue.js性能优化技巧感兴趣的朋友来看看吧。
目录
Functional components child component splitting local variables reuse DOM with v-showkeepalivedefered features time slicing non-reactive data virtual scrolling Summary Reference本文主要参考了Vue.js的核心成员Guillaume Chau在美国Vue conf上19:9 Performance secrets disclosed中分享的主题,分享中提到了Vue.js的9种性能优化技术。
看了他分享的PPT,也看了相关的项目源代码。在深刻理解了它的优化原理后,我将一些优化技巧运用到了平时的工作中,取得了相当不错的效果。
这个分享很实用,但是好像知道和关注的人不多。到目前为止,这个项目只有几百个穷明星。虽然《大老板》的分享已经过去两年了,但是优化技巧并没有过时。为了让更多的人了解和学习实用的技术,我决定对他的分享做二次加工,阐述优化原理,并进行一定程度的拓展和延伸。
本文主要针对Vue.js 2.x版本。毕竟Vue.js 2.x在未来一段时间内还是我们工作中的主流版本。
我建议大家在学习这篇文章的时候,可以拉一下项目的源代码,在本地运行一下,看看优化前后的效果差异。
Functional components
第一个技巧,功能组件,你可以查看这个在线例子。
优化前的组件代码如下:
模板
div class=cell
div v-if=value class=on/div
第五节-else class= off /节
/div
/模板
脚本
导出默认值{
道具:[值],
}
/脚本
优化后的组件代码如下:
模板功能
div class=cell
div v-if= props . value class= on /div
第五节-else class= off /节
/div
/模板
然后我们在每个父组件中渲染优化前后的800个组件,通过修改每一帧中的数据来触发组件的更新。我们打开Chrome的性能面板记录他们的性能,得到如下结果。
优化前:
优化后:
对比这两张图,可以看出优化前执行脚本比优化后要多花时间,而且我们知道JS引擎是单线程运行机制,JS线程会阻塞UI线程,所以当脚本执行时间过长时,就会阻塞渲染,造成页面卡顿。优化后的脚本执行时间短,所以性能更好。
那么,为什么带有功能组件的JS执行时间变短了呢?这要从功能组件的实现原理说起。你可以把它理解为一个函数,可以根据你传递的上下文数据渲染生成一片DOM。
功能组件不同于常见的对象类型组件,它不会被视为真正的组件。我们知道,在打补丁过程中,如果一个节点是组件vnode,子组件的初始化过程就会递归执行;而功能组件的渲染生成普通的vnode,没有递归子组件的过程,渲染开销会低很多。
因此,功能组件不会有状态、响应数据和生命周期挂钩功能。你可以把它看作是从一个通用的组件模板中剥离出一部分DOM,通过函数来渲染,这是一种DOM级别的复用。
Child component splitting
第二个技巧,子组件拆分,你可以查看这个在线例子。
优化前的组件代码如下:
模板
div:style= { opacity:number/300 }
div{{ heavy() }}/div
/div
/模板
脚本
导出默认值{
道具:[数字],
方法:{
重型(){
常数n=100000
让结果=0
for(设I=0;I n;i ) {
result=math . sqrt(math . cos(math . sin(42))。
}
回送结果
}
}
}
/脚本
优化后的组件代码如下:
模板
div:style= { opacity:number/300 }
ChildComp/
/div
/模板
脚本
导出默认值{
组件:{
子组件:{
方法:{
重型(){
常数n=100000
让结果=0
for(设I=0;I n;i ) {
result=math . sqrt(math . cos(math . sin(42))。
}
回送结果
},
},
渲染(h) {
返回h(div ,this.heavy())
}
}
},
道具:[数字]
}
/脚本
然后我们在每个父组件中渲染优化前后的300个组件,在每一帧中,我们修改数据来触发组件的更新。我们打开Chrome的性能面板记录他们的性能,得到如下结果。
优化前:
优化后:
对比这两张图,可以看到优化后的脚本执行时间明显少于优化前,所以性能体验更好。
那么为什么会有区别呢?让我们看看优化前的组件。示例通过一个重函数模拟一个耗时的任务,这个函数每渲染一次就执行一次,所以每次渲染组件都要花很长时间执行JavaScript。
优化的方式是用ChildComp封装这个耗时任务的执行逻辑,这个繁重的函数。因为Vue的更新是组件粒度的,虽然每一帧都是父组件通过数据修改重新渲染的,但是ChildComp不会重新渲染,因为它内部没有响应的数据变化。因此,优化后的组件不会在每次渲染时都执行耗时的任务,自然执行的JavaScript时间会减少。
但是,我对这种优化方法提出了一些不同的看法。详情请点击本期。我认为在这种情况下,使用计算属性进行优化比拆分子组件更好。由于计算属性本身的缓存属性,耗时的逻辑将只在第一次呈现时执行,使用计算属性呈现子组件没有额外的开销。
在实际工作中,会有很多使用计算属性来优化性能的场景。毕竟也体现了一种空间换时间的优化思路。
Local variables
第三个技巧,局部变量,你可以看看这个网上的例子。
优化前的组件代码如下:
模板
div:style= { opacity:start/300 } { { result } }/div
/模板
脚本
导出默认值{
道具:[开始],
计算值:{
base () {
返回42
},
结果(){
让结果=this.start
for(设I=0;i 1000i ) {
result=math . sqrt(math . cos(math . sin(this . base)))this . base * this . base this . base . base * 2 this . base * 3
}
回送结果
},
},
}
/脚本
优化后的组件代码如下:
模板
div:style= { opacity:start/300 } { { result } }/div
/模板
脚本
导出默认值{
道具:[开始],
计算值:{
base () {
返回42
},
结果({ base,start }) {
让结果=开始
for(设I=0;i 1000i ) {
result=math . sqrt(math . cos(math . sin(base)))base * base base base * 2 base * 3
}
回送结果
},
},
}
/脚本
然后我们在每个父组件中渲染优化前后的300个组件,在每一帧中,我们修改数据来触发组件的更新。我们打开Chrome的性能面板记录他们的性能,得到如下结果。
优化前:
优化后:
对比这两张图,可以看到优化后的脚本执行时间明显少于优化前,所以性能体验更好。
这主要是优化前后组件的计算属性结果实现的差异。优化前,组件在计算过程中多次访问this.base,而优化后,组件在计算前用本地变量base缓存this.base,然后直接访问base变量。
那么为什么这种差异会造成性能的差异呢?原因是每次访问this.base,因为this.base是一个响应式对象,它的getter会被触发,然后执行依赖集合的相关逻辑代码。类似的逻辑执行得更频繁。如示例中,数百个组件循环更新,每个组件触发计算的重新计算,然后依赖关系收集的相关逻辑被多次执行,因此性能自然下降。
从需求的角度来说,this.base进行一次依赖收集就足够了。所以我们只需要把它的getter求值结果返回给局部变量base。当我们再次访问该库时,getter不会被触发,依赖集合的逻辑也不会被遵循,性能自然会得到提升。
这是一种非常实用的性能优化技术。因为在开发Vue.js项目的时候,很多人习惯性的一取变量就直接写this.xxx,因为大多数人不会注意到访问this.xxx背后做了什么,当访问次数少的时候,性能问题并不突出,但是一旦访问次数增加,比如一个大周期内多次访问,比如示例场景,性能问题就会出现。
我之前对ZoomUI的表格组件进行性能优化的时候,在渲染表体的时候使用了局部变量的优化技术,并且写了一个性能对比的基准:渲染一个1000 * 10的表格,重新渲染ZoomUI表格更新数据的性能是ElementUI表格的近一倍。
Reuse DOM with v-show
第四招,用v-show复用DOM,可以查一下这个网上的例子。
优化前的组件代码如下:
模板功能
div class=cell
div v-if=props.value class=on
Heavy :n=10000/
/div
第五节-else class=off
Heavy :n=10000/
/部分
/div
/模板
优化后的组件代码如下:
模板功能
div class=cell
div v-show= props . value class= on
Heavy :n=10000/
/div
第五节-show=!props.value class=off
Heavy :n=10000/
/部分
/div
/模板
然后我们在每个父组件中渲染优化前后的200个组件,在每一帧中,我们修改数据来触发组件的更新。我们打开Chrome的性能面板记录他们的性能,得到如下结果。
优化前:
优化后:
对比这两张图,可以看到优化后的脚本执行时间明显少于优化前,所以性能体验更好。
优化前后的主要区别是用v-show指令代替v-if指令来代替组件的显示和隐藏。虽然v-show和v-if在性能上差不多,都是控制组件的显示和隐藏,但是在内部实现上还是有很大差距的。
v-if指令将在编译阶段编译成三元运算符,并进行条件渲染。例如,优化前的组件模板被编译以生成以下渲染函数:
函数render() {
用(这个){
return _c(div ,{
staticClass:“单元”
},[(props.value)?_c(div ,{
staticClass:“开”
},[_c(重,{
属性:{
n: 1万
}
})],1) : _c(section ,{
staticClass:“关闭”
},[_c(重,{
属性:{
n: 1万
}
})], 1)])
}
}
当条件props.value的值发生变化时,会触发相应的组件更新。对于v-if渲染节点,由于新旧节点的不一致,在核心diff算法比较的过程中,会去掉旧的vnode节点,创建一个新的vnode节点,然后创建一个新的重组件,并且会经历初始化、渲染vnode和重组件本身补丁的过程。
所以每次用v-if更新一个组件,都会产生一个新的重子组件。当更多组件更新时,自然会造成性能压力。
而当我们使用v-show指令时,优化后的组件模板被编译生成如下的渲染函数:
函数render() {
用(这个){
return _c(div ,{
staticClass:“单元”
},[_c(div ,{
指令:[{
名称:显示,
raw name:“v-show”,
值:(props.value),
表达式:“props.value”
}],
staticClass:“开”
},[_c(重,{
属性:{
n: 1万
}
})],1),_c(section ,{
指令:[{
名称:显示,
raw name:“v-show”,
值:(!props.value),
表情:!道具.价值
}],
staticClass:“关闭”
},[_c(重,{
属性:{
n: 1万
}
})], 1)])
}
}
当条件props.value的值发生变化时,会触发相应的组件更新。对于v-show渲染的节点,由于新旧Vnode一致,所以只需要一直修补vnode即可。那么它是如何让DOM节点显示和隐藏的呢?
在最初的patchVnode进程中,v-show指令对应的hook函数会在内部更新,然后它会根据v-show指令绑定的值来设置它所作用的DOM元素的style.display的值,从而控制显示和隐藏。
所以相对于v-if不断删除和新建函数的DOM,v-show只更新已有DOM的显式和隐式值,所以v-show的开销比v-if小很多,而且其内部DOM结构越复杂,性能差别越大。
但v-show相比v-if的性能优势是在组件的更新阶段。如果只是在初始化阶段,v-if的性能要高于v-show,因为它只能渲染一个分支,而v-show渲染两个分支,通过style.display控制对应DOM的显示和隐藏
使用v-show时,会渲染分支中的所有组件,并执行相应的生命周期钩子函数,而使用v-if时,不会渲染没有命中的分支中的组件,也不会执行相应的生命周期钩子函数。
因此,你必须了解它们的原理和区别,然后才能在不同的情况下使用适当的说明。
KeepAlive
第五种技术,用KeepAlive组件缓存DOM,可以查看这个在线例子。
优化前的组件代码如下:
模板
div id=应用程序
路由器-视图/
/div
/模板
优化后的组件代码如下:
模板
div id=应用程序
点火电极
路由器-视图/
/保持活力
/div
/模板
当我们点击按钮在简单页面和重度页面之间切换时,会渲染出不同的视图,重度页面的渲染非常耗时。我们打开Chrome的性能面板记录它们的性能,然后分别在优化前后执行上述操作,会得到如下结果。
优化前:
优化后:
对比这两张图,可以看到优化后的脚本执行时间明显少于优化前,所以性能体验更好。
在非优化场景下,我们每次点击按钮切换路由视图,都会重新渲染组件,渲染后的组件会经历组件初始化、渲染、补丁等过程。如果组件很复杂或者嵌套很深,整个渲染将会花费很长时间。
使用KeepAlive后,KeepAlive包装的组件的vnode和DOM会在第一次渲染后缓存,然后下次再次渲染组件时,直接从缓存中获取对应的vnode和DOM,然后进行渲染,无需再经过组件初始化、渲染、打补丁等一系列过程,减少了脚本的执行时间,性能更好。
但是使用KeepAlive组件并不是没有成本的,因为它会占用更多的内存进行缓存,这是空间换时间优化思想的典型应用。
Deferred features
第六个技巧,使用Deferred component延迟批量渲染组件,可以查看一下这个在线例子。
优化前的组件代码如下:
模板
div class=延期关闭
vue icon icon= fitness _ center class= gigant /
h2我是一个沉重的页面/h2
粗v-for=n in 8 :key=n/
Heavy class=超重:n=9999999/
/div
/模板
优化后的组件代码如下:
模板
div class=延迟开启
vue icon icon= fitness _ center class= gigant /
h2我是一个沉重的页面/h2
模板v-if=defer(2)
粗v-for=n in 8 :key=n/
/模板
Heavy v-if=defer(3) class=超重:n=9999999/
/div
/模板
脚本
从“@/mixins/Defer”导入延迟
导出默认值{
mixins: [
Defer(),
],
}
/脚本
当我们点击按钮在简单页面和重度页面之间切换时,会渲染出不同的视图,重度页面的渲染非常耗时。我们打开Chrome的性能面板记录它们的性能,然后分别在优化前后执行上述操作,会得到如下结果。
优化前:
优化后:
对比这两张图可以发现,在优化之前,当我们从简单页面切入到重页面的时候,当一个渲染接近尾声的时候,页面还是由简单页面进行渲染,会给人一种页面卡顿的感觉。优化后,当我们从简单页面切入到重页面时,页面已经在渲染前的位置渲染过一次,重页面是逐渐渲染的。
优化前后的差距主要是后者使用了Defer这个mixin,那么具体是怎么操作的呢?让我们来看看:
导出默认函数(计数=10) {
返回{
data () {
返回{
显示优先级:0
}
},
已安装(){
this.runDisplayPriority()
},
方法:{
runDisplayPriority () {
常数步长=()={
requestAnimationFrame(()={
this . display优先级
if (this.displayPriority count) {
步骤()
}
})
}
步骤()
},
延期(优先){
返回这个。displayPriority=priority
}
}
}
}
延迟的主要思想是将一个组件的渲染分成多次。它在内部维护displayPriority变量,然后通过requestAnimationFrame在每一帧中增加自己,最多加到计数中。那么当displayPriority通过v-if=defer(xxx)增加到xxx时,使用Defer mixin的组件可以控制一些块的呈现。
当你有需要花费时间渲染的组件时,使用Deferred进行渐进式渲染是个不错的主意,可以避免因为JS执行时间太长而导致渲染一次卡顿的现象。
Time slicing
第七个技巧,使用时间切片时间切片切割技术,可以查一下这个网上的例子。
优化前的代码如下:
fetchItems ({ commit },{ items }) {
提交(“clearItems”)
提交(“添加项”,项)
}
优化后的代码如下:
fetchItems ({ commit },{ items,splitCount }) {
提交(“clearItems”)
const queue=新作业队列()
splitArray(items,splitCount)。forEach(
chunk=queue.addJob(done={
//按时间片提交数据。
requestAnimationFrame(()={
提交(“添加项”,块)
完成()
})
})
)
await queue.start()
}
首先,我们通过单击Genterate items按钮创建10,000条假数据,然后分别在时间切片打开和关闭时单击Commit items按钮提交数据,并打开Chrome的Performance面板记录它们的性能。我们将得到以下结果。
优化前:
优化后:
对比这两张图,可以发现优化前的脚本执行总时间比优化后的少。但从实际感知来看,优化前点击提交按钮后,页面会卡死1.2秒左右。优化后页面不会完全卡顿,但是在渲染上还是会有卡顿的感觉。
那么为什么优化前页面会卡死呢?由于一次提交的数据太多,内部JS执行时间过长,阻塞了UI线程,导致页面卡顿。
优化后,页面仍然有堵塞,因为我们拆分数据的粒度是1000块。在这种情况下,仍然有重新渲染组件的压力。我们观察fps才十几,会有卡壳的感觉。通常只要页面的fps达到60,页面就会非常流畅。如果我们把数据拆分的粒度改为100片,基本上fps可以达到50片以上。虽然页面渲染变得更加流畅,但是完成10000条数据的总提交时间还是比较长。
使用时间切片技术可以避免页面卡顿。通常,我们会在这种耗时的任务处理中添加一个加载效果。在这个例子中,我们可以打开加载动画,然后提交数据。通过对比发现,在优化之前,JS已经运行了很长时间,阻塞了UI线程,这个加载动画就不会显示了。优化后,由于我们拆分成多个时间片提交数据,单个JS运行时间缩短,这样加载动画就有机会显示出来。
这里需要注意的一点是,虽然我们使用requestAnimationFrame API来拆解时间片,但是使用RequestAnimated Frame本身并不能保证全帧运行。RequestAnimated Frame保证相应的传入回调函数将在浏览器每次重绘后执行。为了保证满帧,JS一个Tick只能运行不超过17ms。
Non-reactive data
第八个技巧,使用非反应性数据,你可以检查这个在线例子。
预优化代码如下:
const data=items.map(
item=({
id: uid,
数据:项目,
投票:0
})
)
优化后的代码如下:
const data=items.map(
item=optimizeItem(item)
)
函数优化项(项目){
常量项目数据={
id: uid,
投票:0
}
Object.defineProperty(itemData, Data ,{
//标记为非反应性
可配置:假,
值:项目
})
返回项目数据
}
和前面的例子一样,我们先通过点击Genterate items按钮创建10000条假数据,然后分别在部分反应性开启和关闭时点击Commit items按钮提交数据,打开Chrome的性能面板记录它们的性能。我们将得到以下结果。
优化前:
优化后:
对比这两张图,可以看到优化后的脚本执行时间明显少于优化前,所以性能体验更好。
这种差异的原因是,当内部提交数据时,新提交的数据也将被默认定义为有响应的。如果数据的子属性是对象的形式,子属性也将变得递归响应。因此,当提交大量数据时,这个过程就变成了一个耗时的过程。
优化后,我们将新提交的数据中的对象属性数据手动更改为可配置的false,这样通过Object.keys(obj)内部获取对象属性数组将忽略该数据,并且它不会为属性数据定义反应性。由于数据指向一个对象,这样也会减少递归响应的逻辑,相当于减少了这部分的性能损失。数据量越大,这种优化的效果就越明显。
其实像这样的优化方式有很多,比如我们在组件中定义的一些数据,并不是所有的都要在数据中定义。模板中有些数据我们不用,也不需要监控它的变化。我们只想在组件的上下文中共享这些数据。此时,我们可以将这些数据装载到组件实例上,例如:
导出默认值{
已创建(){
this.scroll=null
},
已安装(){
this.scroll=new BScroll(this。$el)
}
}
以便我们可以在组件上下文中共享滚动对象,即使它不是响应对象。
Virtual scrolling
第九个技巧,使用虚拟滚动,你可以查看这个在线例子。
预优化组件的代码如下:
div class=项目编号-v
FetchItemViewFunctional
v-for=项目中的项目
:key=item.id
:item=item
@vote=voteItem(item)
/
/div
优化后的代码如下:
循环滚动器
class=items
:items=items
:item-size=24
模板v-slot=“{ item }”
FetchItemView
:item=item
@vote=voteItem(item)
/
/模板
/recycle-滚动条
和前面的例子一样,我们需要打开视图列表,然后点击Genterate items按钮,创建10000条假数据(注意在线例子最多只能创建1000条数据,但实际上1000条数据并不能很好的体现优化效果,所以我修改了源代码的限制,在本地运行,创建了10000条数据)。然后分别在未优化和RecycleScroller的情况下点击Commit items按钮提交数据,滚动页面,打开Chrome的性能面板记录它们的性能。您将获得以下结果。
优化前:
优化后:
对比这两张图,我们发现在非优化的情况下,一万条数据的fps在滚动的情况下只有个位数,在不滚动的情况下只有十几。原因是非优化场景需要渲染的DOM太多,渲染本身的压力就很大。优化后,即使有10000条数据,在滚动的情况下fps也可以超过30,在不滚动的情况下可以达到60全帧。
这种差异是由于虚拟滚动的实现:仅在视口中呈现DOM。这样渲染的DOM总量很小,自然性能会好很多。
虚拟组件也是Guillaume Chau写的,有兴趣的同学可以研究一下它的源代码实现。它的基本原理是监控滚动事件,动态更新要显示的DOM元素,计算它们在视图中的位移。
虚拟滚动组件并不是没有成本的,因为滚动时需要实时计算,所以会有一定的脚本执行成本。所以,如果列表的数据量不是很大,我们用普通的滚动就够了。
总结
通过这篇文章,希望你能了解到Vue.js的9种性能优化技术,并应用到实际的开发项目中。除了以上技术,还有懒加载图片、懒加载组件、异步组件等常见的性能优化方法。
在做性能优化之前,我们需要分析性能的瓶颈,这样才能因地制宜。另外,性能优化需要数据支持。在你做任何性能优化之前,你需要收集优化前的数据,这样你就可以通过优化后的数据对比看到优化的效果。
希望在以后的开发过程中,你不要仅仅满足于实现需求,还要在编写每一行代码的时候考虑可能的性能影响。
参考资料
[1]vue-9-perf-secrets slidse:https://slides.com/akryum/vueconfus-2019
[2] vue-9-perf-secrets分享演讲视频:3359 www . vue master . com/conferences/vue conf-us-2019/9-perf-secrets-discovered/
[3] vue-9-perf-secrets项目源代码:https://github.com/Akryum/vue-9-perf-secrets
[4] vue-9-perf-secrets在线演示地址:https://vue-9-perf-secrets.netlify.app/
[5]vue-9-perf-秘密讨论问题:https://github.com/Akryum/vue-9-perf-secrets/issues/1
[6]Vue-Virtual-Scroller项目源代码:https://github.com/Akryum/vue-virtual-scroller
关于Vue.js的九个性能优化技巧的这篇文章到此为止(值得收藏)。有关Vue.js性能优化技术的更多信息,请搜索我们以前的文章或继续浏览下面的相关文章。希望大家以后能多多支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。