后端一次性给了 10W 数据,我该如何渲染?

这是一个经典的考察虚拟列表的问题,那么到底什么是虚拟列表?

原理探究

虚拟列表是一种优化长列表性能的技术。它通过只渲染当前可见的区域,而不是将整个列表都渲染出来的方式,减小了页面渲染的负担,从而提高了长列表的滚动和渲染性能。

具体地说,在虚拟列表中,我们只渲染当前可见的一部分列表项,并根据用户滚动或其他操作进行动态调整。例如,对于一个包含1000个列表项的列表,如果当前显示10个列表项,则我们只渲染10个列表项的DOM元素,其他元素则暂时不进行渲染。

当用户滚动列表时,我们根据滚动位置实时计算出需要渲染的列表项,并更新页面上的DOM元素。这样,即便列表非常长,我们也可以保证页面的渲染性能,同时还能提供流畅的滚动体验。

简单来说,虚拟列表就是用 JS 控制渲染的列表项,来避免大规模的 DOM 渲染带来的性能消耗。

其实现原理如下

image-20230323204430810

实战操作

在实现虚拟列表之前,先来看一次性渲染 10W DOM 的性能,对比一下两种渲染方式的性能。

image-20230321231755358

以 Vue3 为例,我们可以这样实现(可以将很多变量提取为 props, 这里就不搞那么麻烦了)

<template>
  <div
    @scroll="handleScroll"
    :style="{
      position: 'relative',
      overflowY: 'auto',
      height: visibleHeight + 'px',
      width: '300px',
    }"
  >
    <div :style="{ height: containerHeight + 'px' }"></div>
    <div
      :style="{
        position: 'absolute',
        top: startOffset + 'px',
        height: visibleHeight + 'px',
      }"
    >
      <div
        v-for="item in visibleData"
        :key="item"
        :style="{ height: rowHeight + 'px' }"
      >
        Row {{ item }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from "vue";

const rowCount = 100000; // 总数据量
const rowHeight = 50; // 单行高度
const visibleHeight = 400; // 可见范围内数据的总高度
const containerHeight = rowCount * rowHeight; // 容器高度
const scrollTop = ref(0); // 虚拟列表距离容器顶部的距离

// 计算可见范围内数据的起始索引和结束索引
const startIndex = computed(() => Math.floor(scrollTop.value / rowHeight));
const endIndex = computed(() =>
  Math.min(Math.ceil((scrollTop.value + visibleHeight) / rowHeight), rowCount)
);

// 计算可见范围内的数据
const visibleData = computed(() =>
  Array.from(
    { length: endIndex.value - startIndex.value },
    (_, index) => startIndex.value + index + 1
  )
);

// 监听滚动事件,并更新 scrollTop 属性
const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop;
};
</script>

其中涉及到的几个变量说明如下:

  • rowCount:数据量统计,真实场景下是根据数据计算出来的;
  • rowHeight:单行高度;
  • visibleHeight:可见高度,即虚拟列表的高度;
  • scrollTop:虚拟列表数据展示部分距离顶部的高度,需要让数据渲染部分始终位于可见区域;
  • startIndex:数据起始坐标;
  • endIndex:数据结束坐标
  • containerHeight:所有数据的高度总和,为了渲染真实的滚动条;
  • visibleData:可见区域的数据。

效果如下

QQ20230322-154041-HD

其 DOM 提现如下

QQ20230321-231348-HD

可以看到页面上元素的个数是固定几条,并没有把全部的数据都渲染出来,此时的首次渲染性能分析如下

image-20230321231934168

可以看到渲染时间大大减少。

此时的虚拟列表还是有点僵硬,滚动起来不自然,我们需要为其添加更加自然的滚动效果。

因为我们的虚拟列表的绝对定位高度始终和容器的滚动距离一致,在视觉上就体现为虚拟列表没有滚动效果,我们可以通过添加下面这个计算属性来实现真实的滚动效果

// 偏移量, 用于还原逼真滚效果
const startOffset = computed(
  () => scrollTop.value - (scrollTop.value % rowHeight)
);

将偏移量中的scrollTop替换为startOffset即可实现平滑滚动。


前端小白