>웹 프론트엔드 >프런트엔드 Q&A >vue의 성능 최적화 방법은 무엇입니까?

vue의 성능 최적화 방법은 무엇입니까?

青灯夜游
青灯夜游원래의
2022-01-10 15:45:2011180검색

성능 최적화 방법은 다음과 같습니다. 1. "slot="slotName"" 대신 "v-slot:slotName"을 사용합니다. 2. "v-for"와 "v-if"를 동시에 사용하지 마세요. 키를 추가하려면 "v-for"를 사용하고 인덱스를 키로 사용하지 마십시오. 4. 지연 렌더링 등을 사용하십시오.

vue의 성능 최적화 방법은 무엇입니까?

이 튜토리얼의 운영 환경: Windows 7 시스템, vue 버전 2.9.6, DELL G3 컴퓨터.

Vue 또는 기타 프레임워크를 사용하는 일상적인 개발에서는 일부 성능 문제에 직면하게 됩니다. Vue가 내부적으로 많은 최적화를 수행하는 데 도움이 되었지만 여전히 적극적으로 피해야 할 몇 가지 문제가 있습니다. 나는 일상 업무에서 성능 문제가 발생하기 쉬운 몇 가지 시나리오와 이러한 문제에 대한 최적화 기술을 인터넷의 다양한 전문가가 작성한 기사에 요약했습니다. 이 기사에서 도움이 되기를 바랍니다.

slot="slotName"v-slot:slotName,而不是slot="slotName"

v-slot是 2.6 新增的语法,具体可查看:Vue2.6,2.6 发布已经是快两年前的事情了,但是现在仍然有不少人仍然在使用slot="slotName"这个语法。虽然这两个语法都能达到相同的效果,但是内部的逻辑确实不一样的,下面来看下这两种方式有什么不同之处。

我们先来看下这两种语法分别会被编译成什么:

使用新的写法,对于父组件中的以下模板:

<child>
  <template v-slot:name>{{name}}</template>
</child>

会被编译成:

function render() {
  with (this) {
    return _c(&#39;child&#39;, {
      scopedSlots: _u([
        {
          key: &#39;name&#39;,
          fn: function () {
            return [_v(_s(name))]
          },
          proxy: true
        }
      ])
    })
  }
}

使用旧的写法,对于以下模板:

<child>
  <template slot="name">{{name}}</template>
</child>

会被编译成:

function render() {
  with (this) {
    return _c(
      &#39;child&#39;,
      [
        _c(
          &#39;template&#39;,
          {
            slot: &#39;name&#39;
          },
          [_v(_s(name))]
        )
      ],
    )
  }
}

通过编译后的代码可以发现,旧的写法是将插槽内容作为 children 渲染的,会在父组件的渲染函数中创建,插槽内容的依赖会被父组件收集(name 的 dep 收集到父组件的渲染 watcher),而新的写法将插槽内容放在了 scopedSlots 中,会在子组件的渲染函数中调用,插槽内容的依赖会被子组件收集(name 的 dep 收集到子组件的渲染 watcher),最终导致的结果就是:当我们修改 name 这个属性时,旧的写法是调用父组件的更新(调用父组件的渲染 watcher),然后在父组件更新过程中调用子组件更新(prePatch => updateChildComponent),而新的写法则是直接调用子组件的更新(调用子组件的渲染 watcher)。

这样一来,旧的写法在更新时就多了一个父组件更新的过程,而新的写法由于直接更新子组件,就会更加高效,性能更好,所以推荐始终使用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 以及模板中都访问了 superCount 属性,这三次访问中,实际上只有第一次即created

v-slot 대신 v-slot:slotName 사용은 2.6의 새로운 구문입니다. 자세한 내용은 다음을 참조하세요. Vue2.6, 2.6은 거의 2년 전에 출시되었지만 많은 사람들이 여전히 slot="slotName" 구문을 사용하고 있습니다. 두 구문 모두 동일한 효과를 얻을 수 있지만 내부 논리는 실제로 다릅니다. 두 방법의 차이점을 살펴보겠습니다. 먼저 이 두 구문이 무엇으로 컴파일되는지 살펴보겠습니다.

새 쓰기 방법을 사용하여 상위 구성 요소의 다음 템플릿에 대해:

// UserProfile.vue
<template>
  <div class="user-profile">{{ name }}</div>
</template>
 
<script>
  export default {
    props: [&#39;name&#39;],
    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 &#39;./components/UserProfile&#39;
 
  export default {
    name: &#39;App&#39;,
    components: { UserProfile },
    data() {
      return {
        list: Array(500)
          .fill(null)
          .map((_, idx) => &#39;Test&#39; + idx)
      }
    },
    beforeMount() {
      this.start = Date.now()
    },
    mounted() {
      console.log(&#39;用时:&#39;, Date.now() - this.start)
    }
  }
</script>
 
<style></style>

는 다음으로 컴파일됩니다.

<template functional>
  <div class="user-profile">{{ props.name }}</div>
</template>

이전 쓰기 방법 사용 , for 다음 템플릿:

<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>

컴파일된 코드에서

기존 작성 방법은 슬롯 콘텐츠를 하위 항목으로 렌더링하는 것이며, 이는 의 렌더링 기능에서 생성됩니다. 상위 구성 요소 및 슬롯 콘텐츠 종속성은 상위 구성 요소에 의해 수집되며(이름의 dep는 상위 구성 요소의 렌더링 감시자에 수집됨) 새 쓰기 방법은 슬롯 콘텐츠를 다음에서 호출될 범위 슬롯에 배치합니다. 하위 구성 요소의 렌더링 기능. 슬롯 콘텐츠의 종속성은 하위 구성 요소에 의해 수집됩니다. 구성 요소 컬렉션(이름의 하위 구성 요소는 하위 구성 요소의 렌더링 감시자를 수집합니다.)

최종 결과는 다음과 같습니다. 이전 작성 방법은 상위 구성 요소의 업데이트를 호출하는 것입니다(상위 구성 요소의 렌더링 감시자 호출). 그런 다음 상위 구성 요소 업데이트 프로세스 중에 하위 구성 요소 업데이트(prePatch => updateChildComponent)가 호출되는 반면, 새로운 작성 방법은 하위 구성 요소 업데이트를 직접 호출하는 것입니다(하위 구성 요소의 렌더링 감시자 호출). 이렇게 기존 작성 방법은 업데이트 시 상위 구성 요소 업데이트 프로세스를 추가하는 반면, 새로운 작성 방법은 하위 구성 요소를 직접 업데이트하므로 더 효율적이고 성능이 더 좋으므로 항상 v를 사용하는 것이 좋습니다. - 슬롯:슬롯 이름 구문.

계산된 속성 사용

🎜이것은 여러 번 언급되었습니다. 계산된 속성의 가장 큰 특징 중 하나는 캐시할 수 있다는 것입니다. 이 캐시는 종속성이 변경되지 않는 한 변경되지 않는다는 것을 의미합니다. 재평가되고 다시 액세스할 때 캐시된 값을 직접 얻을 수 있으므로 복잡한 계산을 수행할 때 성능이 크게 향상될 수 있습니다. 다음 코드를 볼 수 있습니다: 🎜
function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        visible
          ? _c(&#39;UserProfile&#39;, {
              attrs: {
                user: user1
              }
            })
          : _e(),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}
🎜이 예에서 superCount 속성은 Created, Mounted 및 템플릿에서 액세스됩니다. 이 세 가지 액세스 중에서 superCount는 실제로 처음으로 액세스되며, 이는 created입니다. > 평가, count 속성이 변경되지 않았기 때문에 나머지 2번에서는 캐시된 값을 직접 반환합니다. 🎜🎜🎜기능적 컴포넌트 사용🎜🎜🎜일부 컴포넌트의 경우 일부 데이터를 표시하는 데만 사용하고 상태 관리, 데이터 모니터링 등이 필요하지 않다면 기능적 컴포넌트를 사용할 수 있습니다. 기능적 구성 요소는 상태가 없고 인스턴스가 없습니다. 초기화 중에 상태를 초기화하거나 인스턴스를 생성하거나 수명 주기를 처리할 필요가 없으며 상태 저장 구성 요소에 비해 더 가볍고 성능이 더 좋습니다. 기능적 구성 요소의 구체적인 사용법은 공식 문서를 참조하세요: Functional Components 🎜🎜이 최적화를 확인하기 위해 간단한 데모를 작성할 수 있습니다: 🎜
function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        _c(&#39;UserProfile&#39;, {
          directives: [
            {
              name: &#39;show&#39;,
              rawName: &#39;v-show&#39;,
              value: visible,
              expression: &#39;visible&#39;
            }
          ],
          attrs: {
            user: user1
          }
        }),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}
🎜UserProfile 이 구성 요소는 props의 이름만 렌더링한 다음 App.vue에서 호출합니다. 500회, beforeMount에서 마운트까지 걸린 시간을 계산합니다. 이는 500개의 하위 구성 요소(UserProfile)를 초기화하는 데 걸린 시간입니다. 🎜🎜많은 시도 끝에 초기화 시간이 약 30ms인 것을 확인하여 이제 UserProfile을 기능적 구성 요소로 변경합니다. 🎜
el.style.display = value ? el.__vOriginalDisplay : &#39;none&#39;
🎜이번에 여러 번 시도한 결과 초기화 시간은 약 10ms -15ms가 되었습니다. 이는 기능적 구성 요소가 상태 저장 구성 요소보다 성능이 더 우수하다는 것을 보여주기에 충분합니다. 🎜🎜🎜v-show와 v-if를 장면과 함께 사용하세요🎜🎜🎜다음은 v-show와 v-if를 사용하는 두 가지 템플릿입니다🎜
<template>
  <div>
    <component :is="currentComponent" />
  </div>
</template>
<template>
  <div>
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
🎜둘 다 특정 구성 요소의 표시를 제어하거나 DOM/Hide를 제어하는 ​​데 사용됩니다. 성능 차이를 논의하기 전에 먼저 둘 사이의 차이점을 분석해 보겠습니다. 그중 v-if 템플릿은 다음과 같이 컴파일됩니다. 🎜
function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        visible
          ? _c(&#39;UserProfile&#39;, {
              attrs: {
                user: user1
              }
            })
          : _e(),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}

可以看到,v-if 的部分被转换成了一个三元表达式,visible 为 true 时,创建一个 UserProfile 的 vnode,否则创建一个空 vnode,在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话UserProfile也会跟着创建 / 销毁。如果UserProfile组件里有很多 DOM,或者要执行很多初始化 / 销毁逻辑,那么随着 visible 的切换,势必会浪费掉很多性能。这个时候就可以用 v-show 进行优化,我们来看下 v-show 编译后的代码:

function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        _c(&#39;UserProfile&#39;, {
          directives: [
            {
              name: &#39;show&#39;,
              rawName: &#39;v-show&#39;,
              value: visible,
              expression: &#39;visible&#39;
            }
          ],
          attrs: {
            user: user1
          }
        }),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}

v-show被编译成了directives,实际上,v-show 是一个 Vue 内部的指令,在这个指令的代码中,主要执行了以下逻辑:

el.style.display = value ? el.__vOriginalDisplay : &#39;none&#39;

它其实是通过切换元素的 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,所以我们要结合具体业务场景去选一个合适的方式。

使用 keep-alive

在动态组件的场景下:

<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并不是没有缺点,组件被缓存时会占用内存,属于空间和时间上的取舍,在实际开发中要根据场景选择合适的方式。

避免 v-for 和 v-if 同时使用

这一点是 Vue 官方的风格指南中明确指出的一点:Vue 风格指南

如以下模板:

<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
</ul>

会被编译成:

// 简化版
function render() {
  return _c(
    &#39;ul&#39;,
    this.users.map((user) => {
      return user.isActive
        ? _c(
            &#39;li&#39;,
            {
              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发生改变时才会执行这段遍历的逻辑,和之前相比,避免了不必要的性能浪费。

始终为 v-for 添加 key,并且不要将 index 作为的 key

这一点是 Vue 风格指南中明确指出的一点,同时也是面试时常问的一点,很多人都习惯的将 index 作为 key,这样其实是不太好的,index 作为 key 时,将会让 diff 算法产生错误的判断,从而带来一些性能问题,你可以看下 ssh 大佬的文章,深入分析下,为什么 Vue 中不要用 index 作为 key。在这里我也通过一个例子来简单说明下当 index 作为 key 时是如何影响性能的。

看下这个例子:

const Item = {
  name: &#39;Item&#39;,
  props: [&#39;message&#39;, &#39;color&#39;],
  render(h) {
    debugger
    console.log(&#39;执行了Item的render&#39;)
    return h(&#39;div&#39;, { style: { color: this.color } }, [this.message])
  }
}
 
new Vue({
  name: &#39;Parent&#39;,
  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: &#39;a&#39;, color: &#39;#f00&#39;, message: &#39;a&#39; },
        { id: &#39;b&#39;, color: &#39;#0f0&#39;, message: &#39;b&#39; }
      ]
    }
  },
  methods: {
    reverse() {
      this.list.reverse()
    }
  }
}).$mount(&#39;#app&#39;)

这里有一个 list,会渲染出来a b,点击后会执行reverse方法将这个 list 颠倒下顺序,你可以将这个例子复制下来,在自己的电脑上看下效果。

我们先来分析用id作为 key 时,点击时会发生什么,

由于 list 发生了改变,会触发Parent组件的重新渲染,拿到新的vnode,和旧的vnode去执行patch,我们主要关心的就是patch过程中的updateChildren逻辑,updateChildren就是对新旧两个children执行diff算法,使尽可能地对节点进行复用,对于我们这个例子而言,此时旧的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: &#39;a&#39;,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: &#39;b&#39;,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  }
]

执行reverse后的新的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: &#39;b&#39;,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: &#39;a&#39;,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  }
]

此时执行updateChildrenupdateChildren会对新旧两组 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中会执行prePatchprePatch中会更新 props,此时我们的两个节点的propsData是相同的,都为{color: '#0f0',message: '绿色'},这样的话Item组件的 props 就不会更新,Item也不会重新渲染。再回到updateChildren中,会继续执行"移动oldStartVnode节点"的操作,将 DOM 元素。移动到正确位置,其他节点对比也是同样的流程。

可以发现,在整个流程中,只是移动了节点,并没有触发 Item 组件的重新渲染,这样实现了节点的复用。

我们再来看下使用index作为 key 的情况,使用index时,旧的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: 0,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: 1,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  }
]

执行reverse后的新的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: 0,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: 1,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  }
]

这里和id作为 key 时的节点就有所不同了,虽然我们把 list 顺序颠倒了,但是 key 的顺序却没变,在updateChildrensameVnode(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: &#39;test&#39;, message: &#39;test&#39;, id: idx }))

export default {
  data() {
    return {
      heavyData: heavyData
    }
  },
  beforeCreate() {
    this.start = Date.now()
  },
  created() {
    console.log(Date.now() - this.start)
  }
}
</script>

heavyData中有一万条数据,这里统计了下从beforeCreatecreated经历的时间,对于这个例子而言,这个时间基本上就是初始化数据的时间。

我在我个人的电脑上多次测试,这个时间一直在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,所以就跳过了下面的过程,自然就省了很多时间。

实际上,不止初始化数据时有影响,你可以用上面的例子统计下从createdmounted所用的时间,在我的电脑上不使用Object.freeze()时,这个时间是60-70ms,使用Object.freeze()后降到了40-50ms,这是因为在渲染函数中读取heavyData中的数据时,会执行到通过Object.defineProperty定义的getter方法,Vue 在这里做了一些收集依赖的处理,肯定就会占用一些时间,由于使用了Object.freeze()后的数据是非响应式的,没有了收集依赖的过程,自然也就节省了性能。

由于访问响应式数据会走到自定义 getter 中并收集依赖,所以平时使用时要避免频繁访问响应式数据,比如在遍历之前先将这个数据存在局部变量中,尤其是在计算属性、渲染函数中使用,关于这一点更具体的说明,你可以看黄奕老师的这篇文章:Local variables

但是这样做也不是没有任何问题的,这样会导致heavyData下的数据都不是响应式数据,你对这些数据使用computedwatch等都不会产生效果,不过通常来说这种大量的数据都是展示用的,如果你有特殊的需求,你可以只对这种数据的某一层使用Object.freeze(),同时配合使用上文中的延迟渲染、函数式组件等,可以极大提升性能。

模板编译和渲染函数、JSX 的性能差异

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 ? &#39;,true&#39; : &#39;&#39;
  })`
}

这里就是生成代码的关键逻辑,这里会把渲染函数保存在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] !== &#39;string&#39;) {
        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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.