VUE 9个性能优化秘密?(vue-9-perf-secrets)

2,998 阅读4分钟

翻到一个star,VUE 性能优化给大家分享一下。
业精于勤,行成于思.

Vue 9 Perf Secrets

栗子

  • Functional components
  • Child component splitting
  • Local variables
  • Reuse DOM with v-show
  • Keep alive(DOM-Reusing Router View)
  • Deferred features
  • Vuex demo

源代码中有个 Static 被注释,没有达到作者的优化效果。


吃栗子

栗子中都分别写在On,Off的对应组件文件中。对比两份代码。

Functional components

Functional components 官方文档.

FunctionalOff

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

FunctionalOn

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

Child component splitting

ChildOff

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

<script>
export default {
  props: ['number'],
  methods: {
    heavy () {
      const n = 100000
      let result = 0
      for (let i = 0; i < n; i++) {
        result += Math.sqrt(Math.cos(Math.sin(42)))
      }
      return result
    },
  },
}
</script>

ChildOn

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

<script>
export default {
  components: {
    ChildComp: {
      methods: {
        heavy () {
          const n = 100000
          let result = 0
          for (let i = 0; i < n; i++) {
            result += Math.sqrt(Math.cos(Math.sin(42)))
          }
          return result
        },
      },
      render (h) {
        return h('div', this.heavy())
      },
    },
  },
  props: ['number'],
}
</script>

关于一个Issues

ustbhuangyi (附上原文地址),提出例子中性能问题,用了 heavy function,应该采用计算属性 computed。这也是使用中,性能优化可以考虑的点,什么时候该用计算属性。

First, thanks for the awesome Project!

In the Child component splitting demo,the root cause of the performance problem is that we use heavy function during render, it will also be called when the component render.

Instead of using child component splitting, can we use computed property? For example:

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

<script>
export default {
  props: ['number'],
  computed: {
    heavy () {
      const n = 100000
      let result = 0
      for (let i = 0; i < n; i++) {
        result += Math.sqrt(Math.cos(Math.sin(42)))
      }
      return result
    }
  }
}

Because create a child component will have some extra overhead, so computed property here is better ? Computed property will use cache if it's dependencies not change, and I think we should use computed property as much as possible other than method, unless in some special cases.

Local variables

LocalOff

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

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

LocalOn

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

<script>
export default {
  props: ['start'],
  computed: {
    base () {
      return 42
    },
    result ({ base, start }) {
      let result = start
      for (let i = 0; i < 1000; i++) {
        result += Math.sqrt(Math.cos(Math.sin(base))) + base * base + base + base * 2 + base * 3
      }
      return result
    },
  },
}
</script>

Reuse DOM with v-show

HideOff

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

HideOn

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

Keep alive (DOM-Reusing Router View)

<template>
  <Benchmark title="DOM-Reusing Router View" class="keep-alive">
    <template #toolbar>
      <VueGroup v-model="page">
        <VueGroupButton :value="false">Simple page</VueGroupButton>
        <VueGroupButton :value="true">Heavy page</VueGroupButton>
      </VueGroup>

      <PlayToggle v-model="play"/>
    </template>

    <template #on>
      <keep-alive>
        <router-view/>
      </keep-alive>
    </template>

    <template #off>
      <router-view/>
    </template>
  </Benchmark>
</template>

<script>

export default {
  data () {
    return {
      play: false,
    }
  },

  computed: {
    page: {
      get () {
        return this.$route.name === 'bench-keep-alive-heavy'
      },
      set (value) {
        if (value) {
          this.$router.push({ name: 'bench-keep-alive-heavy' })
        } else {
          this.$router.push({ name: 'bench-keep-alive' })
        }
      },
    },
  },

  watch: {
    play (value) {
      if (value) {
        this.togglePage()
      } else {
        clearTimeout(this.$_timer)
      }
    },
  },

  created () {
    this.count = 300
  },

  methods: {
    togglePage () {
      this.page = !this.page
      if (this.play) {
        this.$_timer = setTimeout(this.togglePage, 2000)
      }
    },
  },
}
</script>

<style lang="stylus" scoped>
.keep-alive
  .router-multi-view
    height 100%

  >>>
    .simple-page,
    .deferred-off
      height 100%
      display flex
      flex-direction column
      align-items center
      padding 40px
      box-sizing border-box

      h2:not(:last-child)
        margin-bottom 24px
</style>

Deferred features

DeferredOff

<template>
  <div class="deferred-off">
    <VueIcon icon="fitness_center" class="gigantic"/>

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

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

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

DeferredOn

<template>
  <div class="deferred-on">
    <VueIcon icon="fitness_center" class="gigantic"/>

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

    <template v-if="defer(2)">
      <Heavy v-for="n in 8" :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>

mixin Defer

export default function (count = 10) {
  // @vue/component
  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
      },
    },
  }
}

Vuex demo

最后一个给大家好奇去看代码。渲染10000条数据,一个是creash,优化后的没压力。