十万条数据,后端不分页咋办!(如何优化长列表渲染)
长列表是什么?
- 我们通常把一组数量级很大的数据叫做长列表,比如渲染一组上千条的数据,我们以数组的形式拿到这些信息,然后遍历渲染在页面上;
长列表应该如何渲染?
- 面对这种大量的数据我们通常会采取分页拉取的形式来优化用户体验,比如直截了当的分页器,或者无限滚动,再配合懒加载等方式,这样能够满足大部分长列表的场景
- 但是当我们面对的数据量巨大且无法分页拉取时,上面说的方法就不好用了;在一个移动端的页面上呈现成千上万个dom,除了请求时的挑战,渲染时对于用户的设备性能也是一个极大的考验。
- 我尝试在一个页面中渲染近万条数据
- 最直观可以看到的是,在真实列表中产生了十万多个节点。虽然我们创建DOM这件事本身很简单,但是每个dom都会增加页面在内存、布局、样式、绘制方面额外的成本。如果网页的dom过多会导致低端设备变得卡顿甚至无法使用。(Admittedly, DOM nodes themselves are cheap, but they are not free, as each of them adds extra cost in memory, layout, style and paint. Low-end devices will get noticeably slower if not completely unusable if the website has too big of a DOM to manage.)
- 在这里尝试一种虚拟列表的渲染形式,明显的可以看到,在页面中只产生了几百个dom节点,大大提升了页面的渲染性能
虚拟列表的实现原理
- 虚拟列表实际上就是一种按需渲染的操作。我们在渲染上万条数据时,只需要渲染可视区当中的元素,当页面发生滚动时,监听元素并进行替换,从而达到在上万上亿条数据面前,被页面渲染的也只有不过几百个节点,达到优化长列表大数据渲染的目的。
- 如何实现一个虚拟列表,实际上就是在首屏加载的时候,只加载
可视区域
内需要的元素,当页面滚动时,再动态计算需要被渲染的元素,删除掉消失在视窗中的元素,保持总数一致。 - 前面我们看到在虚拟列表渲染中,只渲染了可视区的数据,所以为了让页面能够正常的滚动,我们需要一个占位的元素,在上面代码的DOM结构中
mod-wraper
充当可视区的角色,mod-phantom
是我们的占位元素,用来形成滚动条,mod-realList
才是我们真正渲染出来的元素列表。我们需要定义一些变量来计算该出现在视窗中的数据。
// 假定需要渲染的数据为 listData[] , 每个元素的高度为 itemHight, 占位Dom的滚动距离为scrollTop;
const wrapperHight = X; //视窗高度
const seatListHeight = list.length * itemHight; //占位Dom高度
const visibleCount = Math.ceil(wrapperHight / itemHeight); // 可视元素数量
const startIndex = Math.floor(scrollTop / itemHeight); // 起始元素
const endIndex = startIndex + visibleCount; // 终止元素
const startOffset = scrollTop; //列表偏移量
const visibleData = listData.slice(startIndex, endIndex); //可视区域数据
- 我们可以利用视窗的高度计算出此时视窗中的开始元素与结束元素,对listData进行裁剪。监听
mod-phantom
的滚动条数据对mod-realList
进行translate,使页面看起来实现了真正的滚动。 - 真实的使用案例当中,我们不会只是渲染一个列表,还会存在其他元素以及宽高的不缺定性,那我们就需要设置更多的变量去进行计算。比如页面中的头部和底部数据,封装好的组件不一定能满足我们的需求,所以只有掌握了才能更好的应用于开发当中。
优化思路
- 由于虚拟列表是实时生成dom,所以有一定回流和重绘的成本,并且由于我们用监听滚动条来实现‘假装滚动’,Rander进程无法及时更新视图,所以在用户滑动过快时会产生页面上只剩背景的问题,我们可以通过提前渲染一些元素来缓解这个问题,比如提前渲染100条,这样用户滑得再快也不会看到白屏了。
- 说到了提前渲染,我们也可以使用这种方式去动态监听不定宽高的列表场景。
适用场景
- 说了这么多感觉虚拟列表是不是很厉害,其实通过实践发现,在几百条数据的情况下,虚拟列表与真实Dom的渲染速率毫无差别。所以我们这里所说的长列表,是几千几万条数据的情况下。