vue性能优化9点建议

3,934 阅读8分钟

1.functional component

第一个优化建议可能大家都知道,下面的一段代码是简单的通过props:value来控制两个element是否创建

<template>
  <div class="cell">
    <div v-if="value" class="on"></div>
    <section v-else class="off"></section>
  </div>
</template>

<script>
export default {
  props: ['value']
}
</script>

因为这个组件没有使用到state,可以通过functional component来优化性能,因为functional component没有state,在创建的时候就减少了state追踪等一系操作带来的性能消耗,提高了性能

// 因为functional component没有this,要使用props.value来访问props
<template functional>
  <div class="cell">
    <div v-if="props.value" class="on"></div>
    <section v-else class="off"></section>
  </div>
</template>

<script>
export default {
  props: ['value']
}
</script>

下面是多次重复销毁两个组件性能的对比,黄色的script区域有了一定程度的减少,浏览器就可以做更多的render或者响应。

2.子组件拆分

直接上代码

<template>
  <div :style="{ opacity: number / 300 }">
    <div>{{ heavy() }}</div>
  </div>
</template>

<script>
export default {
  props: ['number'],
  methods: {
    heavy () { /* 长任务 */ }
  }
}
</script>

当组件随着props:number的变化,组件patch重新渲染的时候,heavy长任务也会重新执行。但是如果能将没有与父组件相互依赖的元素,拆成一个组件,在父组件需要重新渲染的时候,因为与父组件没有依赖子组件并不会跟着重新渲染,响应的性能也能得到提升.

<template>
  <div :style="{ opacity: number / 300 }">
    <ChildComp/>
  </div>
</template>

<script>
export default {
  props: ['number'],
  components: {
    ChildComp: {
      methods: {
        heavy () { /* 长任务在子组件里。 */ }
      },
      render (h) {
        return h('div', this.heavy())
      }
    }
  }
}
</script>

把比较消耗性能且与父组件没有依赖关系的元素,拆成一个子组件,当父组件patch且需要重新渲染的时候,子组件不需要重新渲染,长任务也不会重复执行。父组件重复渲染的次数越多,这种方式提高的性能越大。

3.作用域内变量

跟前面一个类似,

<template>
  <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(this.base)
      }
      return result
    }
  }
}
</script>

computed:result中,循环执行heavy方法,其中另外一个computed属性this.base被重复读取,当你读取一个响应式的属性,像computed的属性,或者state,vue会进行一些逻辑的操作,记录这个响应式的属性是如何被读取的,当这个变量改变的时候,就需要通知读取的方法重新执行。将他先存在另外一个本地的变量中(这时,vue已经记录这个响应式变量被读取,下次发生改变时还是会通知这个方法重新执行),就像一个缓存一样,在重复读取这个本地变量的值的时候就不会像读取一个响应式变量一样每次都需要执行那些逻辑。

<template>
  <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      const base = this.base
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(base)
      }
      return result
    }
  }
}
</script>

在一个方法中大量重复引用一个响应式的变量时,先用一个本地变量存储,可以大幅度的提高性能。

4.让属性不需要响应式

前面介绍了vue对响应式变量存取的时候需要进行一些逻辑操作,也介绍了functional component因为没有state,使得他在创建的时候可以变的快一些。这些都是因为响应式变量,在提供比较便利的响应更新的同时也消耗了一些的性能。特别是一些比较大的对象,或者对象有深的属性层级,这个时候vue就要花更多的精力来让你的对象变的响应式。其实我们也可以告诉vue,在对象中某些属性其实不需要进行响应式操作。

const data = items.map(
  item => optimizeItem(item)
)

function optimizeItem (item) {
  const itemData = {
    id: uid++,
    vote: 0
  }
  Object.defineProperty(itemData, 'data', {
    // 使他没有响应式
    configurable: false,
    value: item
  })
  return itemData
}

还有一些其他办法可以做这个比如说Object.freeze(),但是他他同时会使这个属性变的readonly。 如果有1000个items使部分对象属性不需要响应式可以提高17倍的速度

5.通过v-show重用组件

又是比较熟悉的一个,还是直接上代码

<template functional>
  <div class="cell">
    <div v-if="props.value" class="on">
      <Heavy :n="10000"/>
    </div>
    <section v-else class="off">
      <Heavy :n="10000"/>
    </section>
  </div>
</template>

当组件props.value发生改变的时候div.onsection.off会被重复的销毁在创建,这两个组件里面都包含一个比较重型的组件,每次销毁再创建这个重型组件是比较消耗性能的。相比较于v-if的重复销毁又创建一个组件,v-show只是控制组件的显示和隐藏。

<template functional>
  <div class="cell">
    <div v-show="props.value" class="on">
      <Heavy :n="10000"/>
    </div>
    <section v-show="!props.value" class="off">
      <Heavy :n="10000"/>
    </section>
  </div>
</template>

当一个组件需要被来回切换状态控制显示和不显示,相比较v-if会重复的创建销毁组件,利用v-show控制这个组件的隐藏和展示可以更好的重复的利用这个组件。

6.Keep-alive

和上一个一样,v-show是针对组件的复用,对于需要重复使用的页面可以使用Keep-alive进行重用.

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

这也又是一个内存空间换速度的例子

<template>
  <div id="app">
    <keep-alive>
      <router-view/>
    </keep-alive>
  </div>
</template>

当你可能会需要在两个页面之间来回的切换,可以使用Keep-alive对页面进行重用,提升性能.

7.组件先后加载,防止长任务

我们在之前的之前的博客中有介绍过这种用于处理长任务的方式.比方说我们有一大堆需要消耗大量性能创建的组件

<template>
  <div>
    <h2>I'm an heavy page</h2>

    <Heavy v-for="n in 10" :key="n"/>

    <Heavy class="super-heavy" :n="9999999"/>
  </div>
</template>

如果这些组件在同一时间执行,会形成一个长人物,他会影响浏览器对用户的响应,和影响浏览器渲染页面,页面也会明显的感觉到卡顿,这时候我们把异步的组件先创建,在稍后的时候在去创建另外一部分的组件就可以把一个长任务拆分开,浏览器就可以先渲染前面一个组件,用户也会先看到前面创建的一个组件,用户也不会感觉到页面的卡顿了。

<template>
  <div>
    <h2>I'm an heavy page</h2>

    <template v-if="defer(2)">
      <Heavy v-for="n in 10" :key="n"/>
    </template>

    <Heavy v-if="defer(3)" class="super-heavy" :n="9999999"/>
  </div>
</template>

<script>
import Defer from '@/mixins/Defer'

export default {
  mixins: [
    Defer()
  ]
}
</script>

Defer.js

export default function (count = 10) {
  return {
    data () {
      return {
        displayPriority: 0
      }
    },

    mounted () {
      this.runDisplayPriority()
    },

    methods: {
      runDisplayPriority () {
        const step = () => {
          requestAnimationFrame(() => {
            this.displayPriority++
            if (this.displayPriority < count) {
              step()
            }
          })
        }
        step()
      },

      defer (priority) {
        return this.displayPriority >= priority
      }
    }
  }
}

当我们面对一个长任务需要处理的时候,如果他是几个大组件同时创建造成的,先后执行这些组件是拆分长任务的一个比较好的办法

8.Time slicing

一样是解决长任务的方法。

fetchItems ({ commit }, { items }) {
  commit('clearItems')
  commit('addItems', items)
}

这个方法一次性提交了items.length个对象。可能会创建items.length个组件,跟上面的思路一样,比起一次性提交items.length个数据创建一个长任务,我们可以将items拆分开来提交,也就是将一个长任务切分开来执行.

fetchItems ({ commit }, { items, splitCount }) {
  commit('clearItems')
  const queue = new JobQueue()
  splitArray(items, splitCount).forEach(
    chunk => queue.addJob(done => {
      // Commit array chunks on several frames
      requestAnimationFrame(() => {
        commit('addItems', chunk)
        done()
      })
    })
  )
  // Start and wait for all the jobs
  // to finish
  await queue.start()
}

跟上面类似的做法,当我们有很多个对象赋值以后可能会引发很多的组件在同一时间创建从而创建一个长任务,我们可以分批的将对象提交,从而将长任务,拆分成好几个短任务来执行,给浏览器主线程留出时间来响应渲染。

9.只渲染在屏幕中出现组件

在开发时渲染一个列表是很常见的操作

<div class="items no-v">
  <FetchItemView
    v-for="item of items"
    :key="item.id"
    :item="item"
  />
</div>

如果我们的items特别的多,或者我们的FetchItemView组件非常的大,一个就占了一屏的展示空间,其实我们没必要一下子就渲染那么多items.length个组件,我们只需要渲染屏幕中展示的组件就可以了。有一个库可以帮到我们:virtual-scroller

<recycle-scroller
  class="items"
  :items="items"
  :item-size="24"
>
  <template v-slot="{ item }">
    <FetchItemView
      :item="item"
    />
  </template>
</recycle-scroller>

从而及时你有1000个items也只会渲染在屏幕中展示的item。当然我们不需要库也可以办到这个。这里就不介绍了。这一点主要介绍的是一个思想,在追求极致的vue性能时你要意识到:循环渲染组件的时候,其实不必要的组件可能也被创建了。