问题描述
在使用 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>
在浏览器打开页面并录制查看性能表现,结果如下:
可以发现其中渲染(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 帧进行渲染。
此时再次在加载页面时进行性能录制,结果如下:
可以看到虽然渲染和绘制的时长几乎没有改善,但是从箭头所指区域可以看出此次页面的加载,是分了多批次进行的渲染绘制。
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 帧之后才被渲染。性能表现如下,可以看到红色虚线框内有明显的分段:
One More Thing
本篇的优化原理其实不难,主要是为当类似问题真正在项目中出现时,提供一种解决的思路,而不是一定要立马运用上,毕竟:
“过早优化是万恶之源”(premature optimization is the root of all evil)—— Donald Knuth