当我们遇到需要一次性向页面插入十万条数据的情况下,该如何保证页面不卡顿,维持一定的页面渲染性能
😲 场景:
插入十万条数据,渲染到页面十万条数据
🤔 分析:
我们知道,UI渲染在浏览器渲染进程中属于宏任务
,且涉及到页面的绘制,因此执行完当前的的脚本,进入宏任务阶段后,同时由于数据量大,整个渲染耗费时间较长
📋 方案:
- 把数据分批插入到页面
- 虚拟列表,渲染应该渲染的 ✅(本篇内容本文基于vue实例做介绍)
实际上思路就是,在首屏渲染的时候,只加载可视区域内的列表项,当发生滚动的时候,动态计算:
- 当前展示数据项列表
- 渲染列表的垂直偏移距离
- 更新整个全部数据占位容器的高度
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>
-
infinite-list-container
: 可视区域 -
infinite-list-phantom
:占位容器(生成滚动条) -
infinite-list
:渲染区域,通过设置垂直偏移量,模拟滚动效果
1️⃣ 等高的数据项
数据源model相关
-
itemSize
:数据项高度 -
startIndex
:可渲染区域的,起始索引值 -
endIndex
:可渲染区域的,结束索引值(起始 + 可渲染数据项visibleCount) -
visibleData
:可渲染区域的,列表数据 -
screenHeight
: 可是区域高度 -
startOffsetY
:可渲染区域的垂直偏移距离
更新视图view相关
监听container 可视区域的滚动事件scroll时,拿到最新的scrollTop值,可以计算更新相关值:
- 起始索引
startIndex = Math.ceil(scrollTop / itemSize)
- 可渲染数量
visibleCount = Math.ceil(screenHeight / itemSize)
- 结束索引
endIndex = startIndex + visibleCount
- 可渲染数据
visibleData = listData.slice(startIndex, endIndex)
- 渲染区域偏移距离
startOffsetY = scrollTop - (scrollTop % itemSize)
然而在具体实现的时候,很多列表的各数据项,是不定高度的;因此我们考虑实现动态计算高度
2️⃣ 动态高度的数据项
- 默认数据项高度
estimatedItemSize
用于初始化 - 记录数据项位置和高度
positions
:
this.positions = this.listData.map((d, index) => ({
index,
height: this.estimatedItemSize,
top: index * this.estimatedItemSize,
bottom: (index + 1) * this.estimatedItemSize
}));
更新view视图
监听container 可视区域的滚动事件scroll时,拿到最新的scrollTop值,可以计算更新相关值:
- 起始索引(动态计算):
this.positions.findIndex(item => item.bottom > scrollTop)
- 可渲染数量
visibleCount : Math.ceil(screenHeight/ estimatedItemSize)
- 结束索引
endIndex : startIndex + visibleCount
- 可渲染数据
visibleData : listData.slice(startIndex, endIndex)
- 渲染区域偏移距离
startOffsetY
:this.positions.find(item => item.bottom > scrollTop)
这个起始项的top
值
更新model
每次渲染完成之后,在vue 的 updated钩子($nextTick)里面处理更新和校对操作
-
校对positions: 通过拿到页面dom数据项,拿到节点高度,遍历数组与缓存的
positions
做对比:不同则需要更新该节点的position值(同时,需要更新后续节点的值,因为后一项的top
,其实也是前一项的bottom
) -
校对滚动条长度: 更新占位容器
phantom-list
的实际高度:positions最后一项的bottom
-
校对渲染区域位置更新渲染区域
content-list
的垂直偏移量startOffsetY
然而从最终效果看,在滑动速度较快的情况下,仍然会出现空屏的情况...🤦
因此,我们考虑加上前后两层缓冲区
,前后分别都添加上缓冲区数据,计算visibleData
时,
// AVGSCALE 比如是 0.5
const startIndex = start - (visibleCount * AVGSCALE);
const endIndex = end + (visibleCount * AVGSCALE);
return this.listData.slice(startIndex, endIndex)
🧐思考
目前的更新操作,是放在scroll监听事件中处理,这种高频触发方案,难免会重复计算损耗性能,可以考虑在intersectionObserver
这里监听,在回调方法里面处理相关的更新操作
这两种方案目前是借助js操作的角度,去优化大数据量渲染的性能问题;在css层面,也有相关的优化方案,其中content-visibility
属性,就是一个很有效的属性(但是它的兼容不好🙁)