ホームページ > 記事 > ウェブフロントエンド > vue のパフォーマンス最適化方法にはどのようなものがありますか?
パフォーマンスの最適化方法には次のものが含まれます: 1. "slot="slotName"" の代わりに "v-slot:slotName" を使用します。 2. "v-for" と "v-if" を同時に使用しないでください。 ; 3. 必ず「v-for」にキーを追加し、インデックスをキーとして使用しないでください; 4. 遅延レンダリングなどを使用します。
このチュートリアルの動作環境: Windows7 システム、vue2.9.6 バージョン、DELL G3 コンピューター。
Vue やその他のフレームワークを使用した日常の開発では、多かれ少なかれパフォーマンスの問題に遭遇します。Vue は社内で多くの最適化を行うのに役立ちましたが、率先して取り組む必要がある問題がまだいくつかあります。解決する。避けられる。私の日常業務やインターネット上のさまざまな専門家による記事から、パフォーマンス上の問題が発生しやすいいくつかのシナリオとその最適化手法をまとめましたので、この記事で説明します。
slot="slotName"
##v-slot ## の代わりに を使用してください# は 2.6 で追加された新しい構文です。詳細については、Vue2.6 を参照してください。2.6 はほぼ 2 年前にリリースされましたが、多くの人がまだ使用しています。slot="slotName"
この構文。どちらの構文でも同じ効果を実現できますが、内部ロジックは実際に異なります。2 つのメソッドの違いを見てみましょう。 まず、これら 2 つの構文がどのようなものにコンパイルされるかを見てみましょう:
新しい記述方法を使用して、親コンポーネントの次のテンプレートに対して:
<child> <template v-slot:name>{{name}}</template> </child>
は次のとおりです。コンパイルされる形式:
function render() { with (this) { return _c('child', { scopedSlots: _u([ { key: 'name', fn: function () { return [_v(_s(name))] }, proxy: true } ]) }) } }
古い記述方法を使用すると、次のテンプレートの場合:
<child> <template slot="name">{{name}}</template> </child>
は次のようにコンパイルされます:
function render() { with (this) { return _c( 'child', [ _c( 'template', { slot: 'name' }, [_v(_s(name))] ) ], ) } }
コンパイルされたコードを通じて、次のことがわかります。
old 書き方としては、親コンポーネントのレンダリング関数で作成されるスロットの内容を子としてレンダリングすることになります。スロットの内容の依存関係は親コンポーネントで収集されます(名前の依存関係が収集されます)親コンポーネントのレンダリング ウォッチャーに)、新しい書き方ではスロット コンテンツはscopedSlots に配置され、サブコンポーネントのレンダリング関数で呼び出されます。スロット コンテンツの依存関係はサブコンポーネントによって収集されます。 -component (name の dep はサブコンポーネントのレンダリング ウォッチャーに収集されます)、最終結果は次のようになります: name 属性を変更するとき、古い書き方では親コンポーネントの update (call親コンポーネントのレンダリング ウォッチャー) を呼び出し、親コンポーネントの更新プロセス中に子コンポーネントの更新 (prePatch => updateChildComponent) を呼び出しますが、新しい記述方法は、サブコンポーネントの更新を直接呼び出すことです (サブコンポーネントのレンダリング ウォッチャーを呼び出します)。サブコンポーネント)。 このように、古い書き込み方法では更新時に親コンポーネントの更新処理が追加されますが、新しい書き込み方法は子コンポーネントを直接更新するため、より効率的でパフォーマンスが向上するため、常に使用することをお勧めします。
v-slot:slotName 構文。
<template> <div>{{superCount}}</div> </template> <script> export default { data() { return { count: 1 } }, computed: { superCount() { let superCount = this.count // 假设这里有个复杂的计算 for (let i = 0; i < 10000; i++) { superCount++ } return superCount } } } </script>
この例では、created、mounted、template で superCount 属性にアクセスします。これら 3 つのアクセスのうち、最初のアクセス、つまり
created のみです。 SuperCount が評価されます。count 属性は変更されていないため、他の 2 回ではキャッシュされた値が直接返されます。
この最適化を検証するための簡単なデモを作成できます:
// UserProfile.vue <template> <div class="user-profile">{{ name }}</div> </template> <script> export default { props: ['name'], data() { return {} }, methods: {} } </script> <style scoped></style> // App.vue <template> <div id="app"> <UserProfile v-for="item in list" :key="item" : /> </div> </template> <script> import UserProfile from './components/UserProfile' export default { name: 'App', components: { UserProfile }, data() { return { list: Array(500) .fill(null) .map((_, idx) => 'Test' + idx) } }, beforeMount() { this.start = Date.now() }, mounted() { console.log('用时:', Date.now() - this.start) } } </script> <style></style>
UserProfile このコンポーネントは props の名前のみをレンダリングします。次に、App.vue でこれを 500 回呼び出し、beforeMount からマウントされるまでにかかった時間をカウントします。これは、500 個のサブコンポーネント (UserProfile) を初期化するのにかかる時間です。
何度も試行した結果、消費時間が約 30 ミリ秒であることがわかったので、UserProfile を機能コンポーネントに変更します。
<template functional> <div class="user-profile">{{ props.name }}</div> </template>
この時点で何度も試行した結果、初期化時間はは常に 10 ~ 15 ミリ秒であり、機能コンポーネントがステートフル コンポーネントよりもパフォーマンスが優れていることを示すには十分です。
v-show と v-if をシナリオと組み合わせて使用する<template> <div> <UserProfile :user="user1" v-if="visible" /> <button @click="visible = !visible">toggle</button> </div> </template>
<template> <div> <UserProfile :user="user1" v-show="visible" /> <button @click="visible = !visible">toggle</button> </div> </template>
この両方は、特定のコンポーネントまたは DOM の表示/非表示を制御するために使用されます。パフォーマンスの違いについて説明する前に、まず 2 つの違いを分析しましょう。その中で、v-if テンプレートは次のようにコンパイルされます:
function render() { with (this) { return _c( 'div', [ visible ? _c('UserProfile', { attrs: { user: user1 } }) : _e(), _c( 'button', { on: { click: function ($event) { visible = !visible } } }, [_v('toggle')] ) ], ) } }
可以看到,v-if 的部分被转换成了一个三元表达式,visible 为 true 时,创建一个 UserProfile 的 vnode,否则创建一个空 vnode,在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话UserProfile
也会跟着创建 / 销毁。如果UserProfile
组件里有很多 DOM,或者要执行很多初始化 / 销毁逻辑,那么随着 visible 的切换,势必会浪费掉很多性能。这个时候就可以用 v-show 进行优化,我们来看下 v-show 编译后的代码:
function render() { with (this) { return _c( 'div', [ _c('UserProfile', { directives: [ { name: 'show', rawName: 'v-show', value: visible, expression: 'visible' } ], attrs: { user: user1 } }), _c( 'button', { on: { click: function ($event) { visible = !visible } } }, [_v('toggle')] ) ], ) } }
v-show
被编译成了directives
,实际上,v-show 是一个 Vue 内部的指令,在这个指令的代码中,主要执行了以下逻辑:
el.style.display = value ? el.__vOriginalDisplay : 'none'
它其实是通过切换元素的 display 属性来控制的,和 v-if 相比,不需要在 patch 阶段创建 / 移除节点,只是根据v-show
上绑定的值来控制 DOM 元素的style.display
属性,在频繁切换的场景下就可以节省很多性能。
但是并不是说v-show
可以在任何情况下都替换v-if
,如果初始值是false
时,v-if
并不会创建隐藏的节点,但是v-show
会创建,并通过设置style.display='none'
来隐藏,虽然外表看上去这个 DOM 都是被隐藏的,但是v-show
已经完整的走了一遍创建的流程,造成了性能的浪费。
所以,v-if
的优势体现在初始化时,v-show
体现在更新时,当然并不是要求你绝对按照这个方式来,比如某些组件初始化时会请求数据,而你想先隐藏组件,然后在显示时能立刻看到数据,这时候就可以用v-show
,又或者你想每次显示这个组件时都是最新的数据,那么你就可以用v-if
,所以我们要结合具体业务场景去选一个合适的方式。
在动态组件的场景下:
<template> <div> <component :is="currentComponent" /> </div> </template>
这个时候有多个组件来回切换,currentComponent
每变一次,相关的组件就会销毁 / 创建一次,如果这些组件比较复杂的话,就会造成一定的性能压力,其实我们可以使用 keep-alive 将这些组件缓存起来:
<template> <div> <keep-alive> <component :is="currentComponent" /> </keep-alive> </div> </template>
keep-alive
的作用就是将它包裹的组件在第一次渲染后就缓存起来,下次需要时就直接从缓存里面取,避免了不必要的性能浪费,在讨论上个问题时,说的是v-show
初始时性能压力大,因为它要创建所有的组件,其实可以用keep-alive
优化下:
<template> <div> <keep-alive> <UserProfileA v-if="visible" /> <UserProfileB v-else /> </keep-alive> </div> </template>
这样的话,初始化时不会渲染UserProfileB
组件,当切换visible
时,才会渲染UserProfileB
组件,同时被keep-alive
缓存下来,频繁切换时,由于是直接从缓存中取,所以会节省很多性能,所以这种方式在初始化和更新时都有较好的性能。
但是keep-alive
并不是没有缺点,组件被缓存时会占用内存,属于空间和时间上的取舍,在实际开发中要根据场景选择合适的方式。
这一点是 Vue 官方的风格指南中明确指出的一点:Vue 风格指南
如以下模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id"> {{ user.name }} </li> </ul>
会被编译成:
// 简化版 function render() { return _c( 'ul', this.users.map((user) => { return user.isActive ? _c( 'li', { key: user.id }, [_v(_s(user.name))] ) : _e() }), ) }
可以看到,这里是先遍历(v-for),再判断(v-if),这里有个问题就是:如果你有一万条数据,其中只有 100 条是isActive
状态的,你只希望显示这 100 条,但是实际在渲染时,每一次渲染,这一万条数据都会被遍历一遍。比如你在这个组件内的其他地方改变了某个响应式数据时,会触发重新渲染,调用渲染函数,调用渲染函数时,就会执行到上面的代码,从而将这一万条数据遍历一遍,即使你的users
没有发生任何改变。
为了避免这个问题,在此场景下你可以用计算属性代替:
<template> <div> <ul> <li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li> </ul> </div> </template> <script> export default { // ... computed: { activeUsers() { return this.users.filter((user) => user.isActive) } } } </script> 复制代码
这样只会在users
发生改变时才会执行这段遍历的逻辑,和之前相比,避免了不必要的性能浪费。
这一点是 Vue 风格指南中明确指出的一点,同时也是面试时常问的一点,很多人都习惯的将 index 作为 key,这样其实是不太好的,index 作为 key 时,将会让 diff 算法产生错误的判断,从而带来一些性能问题,你可以看下 ssh 大佬的文章,深入分析下,为什么 Vue 中不要用 index 作为 key。在这里我也通过一个例子来简单说明下当 index 作为 key 时是如何影响性能的。
看下这个例子:
const Item = { name: 'Item', props: ['message', 'color'], render(h) { debugger console.log('执行了Item的render') return h('div', { style: { color: this.color } }, [this.message]) } } new Vue({ name: 'Parent', template: ` <div @click="reverse" class="list"> <Item v-for="(item,index) in list" :key="item.id" :message="item.message" :color="item.color" /> </div>`, components: { Item }, data() { return { list: [ { id: 'a', color: '#f00', message: 'a' }, { id: 'b', color: '#0f0', message: 'b' } ] } }, methods: { reverse() { this.list.reverse() } } }).$mount('#app')
这里有一个 list,会渲染出来a b
,点击后会执行reverse
方法将这个 list 颠倒下顺序,你可以将这个例子复制下来,在自己的电脑上看下效果。
我们先来分析用id
作为 key 时,点击时会发生什么,
由于 list 发生了改变,会触发Parent
组件的重新渲染,拿到新的vnode
,和旧的vnode
去执行patch
,我们主要关心的就是patch
过程中的updateChildren
逻辑,updateChildren
就是对新旧两个children
执行diff
算法,使尽可能地对节点进行复用,对于我们这个例子而言,此时旧的children
是:
;[ { tag: 'Item', key: 'a', propsData: { color: '#f00', message: '红色' } }, { tag: 'Item', key: 'b', propsData: { color: '#0f0', message: '绿色' } } ]
执行reverse
后的新的children
是:
;[ { tag: 'Item', key: 'b', propsData: { color: '#0f0', message: '绿色' } }, { tag: 'Item', key: 'a', propsData: { color: '#f00', message: '红色' } } ]
此时执行updateChildren
,updateChildren
会对新旧两组 children 节点的循环进行对比:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 对新旧节点执行patchVnode // 移动指针 } else if (sameVnode(oldEndVnode, newEndVnode)) { // 对新旧节点执行patchVnode // 移动指针 } else if (sameVnode(oldStartVnode, newEndVnode)) { // 对新旧节点执行patchVnode // 移动oldStartVnode节点 // 移动指针 } else if (sameVnode(oldEndVnode, newStartVnode)) { // 对新旧节点执行patchVnode // 移动oldEndVnode节点 // 移动指针 } else { //... } }
通过sameVnode
判断两个节点是相同节点的话,就会执行相应的逻辑:
function sameVnode(a, b) { return ( a.key === b.key && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error))) ) }
sameVnode
主要就是通过 key 去判断,由于我们颠倒了 list 的顺序,所以第一轮对比中:sameVnode(oldStartVnode, newEndVnode)
成立,即旧的首节点和新的尾节点是同一个节点,此时会执行patchVnode
逻辑,patchVnode
中会执行prePatch
,prePatch
中会更新 props,此时我们的两个节点的propsData
是相同的,都为{color: '#0f0',message: '绿色'}
,这样的话Item
组件的 props 就不会更新,Item
也不会重新渲染。再回到updateChildren
中,会继续执行"移动oldStartVnode节点"
的操作,将 DOM 元素。移动到正确位置,其他节点对比也是同样的流程。
可以发现,在整个流程中,只是移动了节点,并没有触发 Item 组件的重新渲染,这样实现了节点的复用。
我们再来看下使用index
作为 key 的情况,使用index
时,旧的children
是:
;[ { tag: 'Item', key: 0, propsData: { color: '#f00', message: '红色' } }, { tag: 'Item', key: 1, propsData: { color: '#0f0', message: '绿色' } } ]
执行reverse
后的新的children
是:
;[ { tag: 'Item', key: 0, propsData: { color: '#0f0', message: '绿色' } }, { tag: 'Item', key: 1, propsData: { color: '#f00', message: '红色' } } ]
这里和id
作为 key 时的节点就有所不同了,虽然我们把 list 顺序颠倒了,但是 key 的顺序却没变,在updateChildren
时sameVnode(oldStartVnode, newStartVnode)
将会成立,即旧的首节点和新的首节点相同,此时执行patchVnode -> prePatch -> 更新props
,这个时候旧的 propsData 是{color: '#f00',message: '红色'}
,新的 propsData 是{color: '#0f0',message: '绿色'}
,更新过后,Item 的 props 将会发生改变,会触发 Item 组件的重新渲染。
这就是 index 作为 key 和 id 作为 key 时的区别,id 作为 key 时,仅仅是移动了节点,并没有触发 Item 的重新渲染。index 作为 key 时,触发了 Item 的重新渲染,可想而知,当 Item 是一个复杂的组件时,必然会引起性能问题。
上面的流程比较复杂,涉及的也比较多,可以拆开写好几篇文章,有些地方我只是简略的说了一下,如果你不是很明白的话,你可以把上面的例子复制下来,在自己的电脑上调式,我在 Item 的渲染函数中加了打印日志和 debugger,你可以分别用 id 和 index 作为 key 尝试下,你会发现 id 作为 key 时,Item 的渲染函数没有执行,但是 index 作为 key 时,Item 的渲染函数执行了,这就是这两种方式的区别。
延迟渲染就是分批渲染,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑:
<template> <p> <!-- Heavy组件初始化时需要执行很复杂的逻辑,执行大量计算 --> <Heavy1 /> <Heavy2 /> <Heavy3 /> <Heavy4 /> </p> </template>
这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分:
参考黄轶老师揭秘 Vue.js 九个性能优化技巧中的代码:
<template> <p> <Heavy v-if="defer(1)" /> <Heavy v-if="defer(2)" /> <Heavy v-if="defer(3)" /> <Heavy v-if="defer(4)" /> </p> </template> <script> export default { data() { return { displayPriority: 0 } }, mounted() { this.runDisplayPriority() }, methods: { runDisplayPriority() { const step = () => { requestAnimationFrame(() => { this.displayPriority++ if (this.displayPriority < 10) { step() } }) } step() }, defer(priority) { return this.displayPriority >= priority } } } </script>
其实原理很简单,主要是维护displayPriority
变量,通过requestAnimationFrame
在每一帧渲染时自增,然后我们就可以在组件上通过v-if="defer(n)"
使displayPriority
增加到某一值时再渲染,这样就可以避免 js 执行时间过长导致的卡顿问题了。
在 Vue 组件初始化数据时,会递归遍历在 data 中定义的每一条数据,通过Object.defineProperty
将数据改成响应式,这就意味着如果 data 中的数据量很大的话,在初始化时将会使用很长的时间去执行Object.defineProperty
, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间,看下这个例子:
<template> <p> <ul> <li v-for="item in heavyData" :key="item.id">{{ item.name }}</li> </ul> </p> </template> <script> // 一万条数据 const heavyData = Array(10000) .fill(null) .map((_, idx) => ({ name: 'test', message: 'test', id: idx })) export default { data() { return { heavyData: heavyData } }, beforeCreate() { this.start = Date.now() }, created() { console.log(Date.now() - this.start) } } </script>
heavyData
中有一万条数据,这里统计了下从beforeCreate
到created
经历的时间,对于这个例子而言,这个时间基本上就是初始化数据的时间。
我在我个人的电脑上多次测试,这个时间一直在40-50ms
,然后我们通过Object.freeze()
方法,将heavyData
变为非响应式的再试下:
//... data() { return { heavyData: Object.freeze(heavyData) } } //...
改完之后再试下,初始化数据的时间变成了0-1ms
,快了有40ms
,这40ms
都是递归遍历heavyData
执行Object.defineProperty
的时间。
那么,为什么Object.freeze()
会有这样的效果呢?对某一对象使用Object.freeze()
后,将不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
而 Vue 在将数据改造成响应式之前有个判断:
export function observe(value, asRootData) { // ...省略其他逻辑 if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } // ...省略其他逻辑 }
这个判断条件中有一个Object.isExtensible(value)
,这个方法是判断一个对象是否是可扩展的,由于我们使用了Object.freeze()
,这里肯定就返回了false
,所以就跳过了下面的过程,自然就省了很多时间。
实际上,不止初始化数据时有影响,你可以用上面的例子统计下从created
到mounted
所用的时间,在我的电脑上不使用Object.freeze()
时,这个时间是60-70ms
,使用Object.freeze()
后降到了40-50ms
,这是因为在渲染函数中读取heavyData
中的数据时,会执行到通过Object.defineProperty
定义的getter
方法,Vue 在这里做了一些收集依赖的处理,肯定就会占用一些时间,由于使用了Object.freeze()
后的数据是非响应式的,没有了收集依赖的过程,自然也就节省了性能。
由于访问响应式数据会走到自定义 getter 中并收集依赖,所以平时使用时要避免频繁访问响应式数据,比如在遍历之前先将这个数据存在局部变量中,尤其是在计算属性、渲染函数中使用,关于这一点更具体的说明,你可以看黄奕老师的这篇文章:Local variables
但是这样做也不是没有任何问题的,这样会导致heavyData
下的数据都不是响应式数据,你对这些数据使用computed
、watch
等都不会产生效果,不过通常来说这种大量的数据都是展示用的,如果你有特殊的需求,你可以只对这种数据的某一层使用Object.freeze()
,同时配合使用上文中的延迟渲染、函数式组件等,可以极大提升性能。
Vue 项目不仅可以使用 SFC 的方式开发,也可以使用渲染函数或 JSX 开发,很多人认为仅仅是只是开发方式不同,却不知这些开发方式之间也有性能差异,甚至差异很大,这一节我就找些例子来说明下,希望你以后在选择开发方式时有更多衡量的标准。
其实 Vue2 模板编译中的性能优化不多,Vue3 中有很多,Vue3 通过编译和运行时结合的方式提升了很大的性能,但是由于本篇文章讲的是 Vue2 的性能优化,并且 Vue2 现在还是有很多人在使用,所以我就挑 Vue2 模板编译中的一点来说下。
下面这个模板:
<p>你好! <span>Hello</span></p>
会被编译成:
function render() { with (this) { return _m(0) } }
可以看到和普通的渲染函数是有些不一样的,下面我们来看下为什么会编译成这样的代码。
Vue 的编译会经过optimize
过程,这个过程中会标记静态节点,具体内容可以看黄奕老师写的这个文档:Vue2 编译 - optimize 标记静态节点。
在codegen
阶段判断到静态节点的标记会走到genStatic
的分支:
function genStatic(el, state) { el.staticProcessed = true const originalPreState = state.pre if (el.pre) { state.pre = el.pre } state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) state.pre = originalPreState return `_m(${state.staticRenderFns.length - 1}${ el.staticInFor ? ',true' : '' })` }
这里就是生成代码的关键逻辑,这里会把渲染函数保存在staticRenderFns
里,然后拿到当前值的下标生成_m
函数,这就是为什么我们会得到_m(0)
。
这个_m
其实是renderStatic
的缩写:
export function renderStatic(index, isInFor) { const cached = this._staticTrees || (this._staticTrees = []) let tree = cached[index] if (tree && !isInFor) { return tree } tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this ) markStatic(tree, `__static__${index}`, false) return tree } function markStatic(tree, key) { if (Array.isArray(tree)) { for (let i = 0; i < tree.length; i++) { if (tree[i] && typeof tree[i] !== 'string') { markStaticNode(tree[i], `${key}_${i}`, isOnce) } } } else { markStaticNode(tree, key, isOnce) } } function markStaticNode(node, key, isOnce) { node.isStatic = true node.key = key node.isOnce = isOnce }
renderStatic
的内部实现比较简单,先是获取到组件实例的_staticTrees
,如果没有就创建一个,然后尝试从_staticTrees
上获取之前缓存的节点,获取到的话就直接返回,否则就从staticRenderFns
上获取到对应的渲染函数执行并将结果缓存到_staticTrees
上,这样下次再进入这个函数时就会直接从缓存上返回结果。
拿到节点后还会通过markStatic
将节点打上isStatic
等标记,标记为isStatic
的节点会直接跳过patchVnode
阶段,因为静态节点是不会变的,所以也没必要 patch,跳过 patch 可以节省性能。
通过编译和运行时结合的方式,可以帮助我们很好的提升应用性能,这是渲染函数 / JSX 很难达到的,当然不是说不能用 JSX,相比于模板,JSX 更加灵活,两者有各自的使用场景。在这里写这些是希望能给你提供一些技术选型的标准。
Vue2 的编译优化除了静态节点,还有插槽,createElement 等。
【相关推荐:vue.js教程】
以上がvue のパフォーマンス最適化方法にはどのようなものがありますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。