
- Scripting: Javascript逻辑处理时间
- Rendering: UI渲染时间
有人会问,不应该是数据量多导致渲染慢吗? 那为啥耗时都在Scripting呢?
那是因为Vue3在渲染前会进行一系列的Javascript逻辑处理,包括
- 数据创建
- 模板解析
- 虚拟DOM创建
- 虚拟DOM转真实DOM
不过最耗时的是可能就是后两步了,所以需要针对这个问题,做一个解决方案
页面代码
<template>
<Row>
<Col :span="8" v-for="dataItem in dataSource">
<div style="height: 800px; overflow-y: auto">
<List item-layout="horizontal" :data-source="dataItem">
<template #renderItem="{item}">
<ListItem>
<ListItemMeta :description="item.description">
<template #title>
<div>{{ item.title }}</div>
</template>
<template #avatar>
<Avatar :src="item.avatar"/>
</template>
</ListItemMeta>
</ListItem>
</template>
</List>
</div>
</Col>
</Row>
</template>
<script setup lang="ts">
import { shallowRef, onMounted } from 'vue';
import { Row, Col, List, ListItem, ListItemMeta, Avatar } from 'ant-design-vue';
const dataSource = shallowRef<any[]>([[], [],[]]);
const fetchData = () => {
dataSource.value = [
new Array(200).fill({
title: '这是数据1的标题',
description: '这是数据1的描述',
avatar: 'https://next.antdv.com/assets/logo.1ef800a8.svg'
}),
new Array(200).fill({
title: '这是数据2的标题',
description: '这是数据2的描述',
avatar: 'https://next.antdv.com/assets/logo.1ef800a8.svg'
}),
new Array(200).fill({
title: '这是数据3的标题',
description: '这是数据3的描述',
avatar: 'https://next.antdv.com/assets/logo.1ef800a8.svg'
})
]
}
onMounted(() => {
fetchData();
})
</script>
# 懒加载分页?虚拟滚动?不行
很多人第一想象到的解决方案就是做分页,其实分页和懒加载都是一个道理,就是按需去加载数据,但是不好意思,后端接口是老接口并且没有做分页,团队不想耗费后端精力去重构这个接口,所以这条路就别想了!!
又有人说虚拟滚动,这么说吧,虚拟滚动用在性能好的电脑上效果是很不错的,如果用在性能差的电脑上,那么效果会非常糟糕,毫无疑问虚拟滚动确实会减少首屏加载的时间,但是性能差的电脑滚动快了,会有白屏现象,而且很严重,,熟悉虚拟滚动的人都知道,虚拟滚动是根据滚动时间去重新计算渲染区域的,这个计算时需要时间的,但是用户滚动是很快的,性能差的电脑根本计算不过来所以导致会有短暂白屏现象。。
又有人说虚拟滚动不是可以做预加载吗?可以解决短暂白屏现象。还是那句话,在性能好的电脑上确实可以,但是性能差的电脑上,你预渲染再多也没用,该白屏还是得白屏
# 分片渲染
不能做分片,不能做懒加载,不能做虚拟滚动,那么咋办呢?我回家挨说选择了分片渲染进行渲染,也就是在浏览器渲染的每一帧中去不断渲染列表数据,一直渲染出整个列表数据位置。
这样做就能保证首屏时不会一股脑把整个列表都渲染出来,而且是先进侯爷后,在慢慢把所有列表都渲染完成。
# 实施
要这么才能保证在每一帧中去控制列表渲染呢?可以使用 requestAnimationFrame,我们先封装一个useDefer
- frame: 记录当前的帧数
- checkIsRedner: 拿列表每一项的索引去跟当前帧数比较,到达了制定索引帧数才能渲染这一项
import { readonly, ref } from 'vue';
export const useDefer = () => {
const frame = ref(0);
const updateFrame = () => {
requesetAnimationFrame(() => {
// 每一帧递增
frame.value++;
updateFrame();
})
}
updateFrame();
const checkIsRedner =(index: number) => {
// 判断是否需要渲染那
return frame.value >= index;
}
return {
frame: readonly(frame),
checkIsRender
}
}
页面里直接使用这个HOOKS即可
import { useDefer } from './useDefer';
// 使用hooks
const { checkIsRender } = useDefer();
<Row>
<Col :span="8" v-for="dataItem in dataSource">
<div style="height: 800px; overflow-y: auto">
<List item-layout="horizontal" :data-source="dataItem">
<template #renderItem="{item, index}">
<!-- 加一个 v-if 判断 -->
<template v-if="checkIsRender(index)">
<ListItem>
<ListItemMeta :description="item.description">
<template #title>
<div>{{ item.title }}</div>
</template>
<template #avatar>
<Avatar :src="item.avatar"/>
</template>
</ListItemMeta>
</ListItem>
</template>
</template>
</List>
</div>
</Col>
</Row>
这样就能保证了达到一定帧数时,才去渲染一定的列表数据,我们来看看效果,可以看到首屏快了很多 从8s->2s,因为首屏并不会一股脑加载所有数据,而是逐步加载,这一点看滚动条的变化就知道了

滚动条一直在变,因为数据在不断逐步渲染
# 优化点
我们在完成一个功能后,可以看出这个功能有什么
- 列表渲染完毕后,可以停止当前帧的计算
- 现在是一帧渲染一条数据,能否控制一帧渲染多条数据?
import { onUnmounted, readonly, ref } from 'vue';
export const useDefer = (max: number, count: number = 1) => {
const frame = ref(0);
// 记录raf的id
let rafId: number | null = null;
const updateFrame = () => {
rafId = requestAnimationFrame(() => {
// 每一帧的时候递增 count
frame.value = frame.value + count;
// 渲染完就不计算计算了
if(frame.vlaue >= max) return;
updateFrame();
})
}
updateFrame();
const checkIsRender = (index: number) => {
// 判断是否需要渲染那
return frame.value >= index;
}
onUnmounted(() => {
// 组件销毁时取消
if(rafId) {
cancelAnimationFrame(rafId)
}
})
return {
frame: readonly(frame),
checkIsRender
}
}
import { useDefer } from './useDefer'
// 使用hooks
const { checkIsRender } = useDefer(200,10)