vue 项目优化技巧之延时加载

585 阅读3分钟

问题描述

在使用 vue 开发的项目中,如果某个页面有着大量的节点,可能会导致页面渲染速度变慢。比如我写了一个 Section 组件,该组件没有 js 代码,只是循环生成 20000 个 div

<!-- 子组件 -->
<template>
  <div class="box">
    <div v-for="item in 20000">
      <div class="item"></div>
    </div>
  </div>
</template>

<style scoped>
  .box {
    display: flex;
    flex-wrap: wrap;
    border: 1px solid #999;
    margin-bottom: 4px;
  }
  .item {
    width: 6px;
    height: 2px;
    background-color: antiquewhite;
    margin: 1px;
  }
</style>

在页面引入 Section 后循环渲染 3 个:

<!-- src\App.vue -->
<template>
  <div id="app">
    <div v-for="item in 3">
      <Section />
    </div>
  </div>
</template>

在浏览器打开页面并录制查看性能表现,结果如下:

1.png

可以发现其中渲染(Rendering)和绘制(Painting)的耗时是比较多的。

优化方案

既然每个 Section 组件内的 div 过多会导致渲染与绘制耗时较久,那么页面在加载 3 个Section 时,我们就可以想办法让它们分批次地加载,让页面尽快地有内容可以展示。总体思路就是借助 requestAnimationFrame() 这个 api,控制 3 个 Section 在不同的帧依次渲染。具体实现根据 vue2 和 vue3 有些写法上的区别,下面分开讲述。

vue2

vue2 项目中我们可以写一个可以接收参数的 mixin 文件 defer.js:

// src\mixins\defer.js
export default function (totalFrame) {
  return {
    data() {
      return {
        frameNum: 0
      }
    },
    mounted() {
      this.updateFrameNum()
    },
    methods: {
      updateFrameNum() {
        if (++this.frameNum < totalFrame)
          requestAnimationFrame(this.updateFrameNum)
      },
      isRender(showFrame) {
        return this.frameNum >= showFrame
      }
    }
  }
}

其中:

  • frameNum 用于记录当前的帧数;
  • updateFrameNum 方法就是让 frameNum 在小于传入的 totalFrame 之前,每一帧都进行自增操作;
  • isRender 方法返回一个布尔值,使用时会传入一个 number 类型的数字 showFrame,如果当前帧数大于等于 showFrame,则会返回 true。在页面使用时会配合 v-if 来控制组件何时渲染。

在页面中,引入 defer:

<!-- src\App.vue -->
<template>
  <div id="app">
    <div v-for="item in 3">
      <Section v-if="isRender(item)" />
    </div>
  </div>
</template>

<script>
  import defer from './mixins/defer'
  import Section from './components/Section.vue'
  export default {
    mixins: [defer(3)],
    components: {
      Section
    }
  }
</script>

第 14 行的 defer(3) 中传入了 3,则 defer.js 中的 frameNum 会从 0 逐帧增加到 3。第 5 行的 v-if="isRender(item)",就是让 <Section> 分别在第 1 帧、第 2 帧和第 3 帧进行渲染。

此时再次在加载页面时进行性能录制,结果如下:

2.png

可以看到虽然渲染和绘制的时长几乎没有改善,但是从箭头所指区域可以看出此次页面的加载,是分了多批次进行的渲染绘制。

vue3

在 vue3 项目中,思路是一样的,只不过是改为自定义一个 hook 函数,其中用到一些 vue3 的组合 api,来替代 vue2 项目中的 mixin 写法:

// src\hooks\defer.ts
import { onMounted, ref } from "vue"

export default function (totalFrame: number) {
  const frameNum = ref(0)
  onMounted(() => {
    updateFrameNum()
  })
  function updateFrameNum() {
    if (++frameNum.value < totalFrame) requestAnimationFrame(updateFrameNum)
  }
  function isRender(showFrame: number) {
    return frameNum.value >= showFrame
  }
  return { isRender }
}

在页面引入 hook 并使用:

<!-- src\App.vue -->
<script setup lang="ts">
  import Section from './components/Section.vue'
  import defer from './hooks/defer'

  const { isRender } = defer(300)
</script>

<template>
  <div v-for="item in 3">
    <Section v-if="isRender(item * 100)" />
  </div>
</template>

这一次我们更改了传参,改为 defer(300)isRender(item * 100),可以更直观地看出延时加载的效果:

可以看到 3 个 <Section > 分别在第 100 帧、200 帧和 300 帧之后才被渲染。性能表现如下,可以看到红色虚线框内有明显的分段:

4.png

One More Thing

本篇的优化原理其实不难,主要是为当类似问题真正在项目中出现时,提供一种解决的思路,而不是一定要立马运用上,毕竟:

“过早优化是万恶之源”(premature optimization is the root of all evil)—— Donald Knuth

感谢.gif 点赞.png