如何优化大量数据的渲染

828 阅读1分钟

长列表的渲染问题

假如我们想要渲染一个列表,其中有很多数据,比如用户的购买记录,我们立马可以想到直接用 v-for 或者遍历数组返回 JSX 的形式来展示

这种做法没问题,但是我们需要考虑一个渲染速度的问题,假如用户有 1000 条记录甚至更多,我们直接进行渲染的话要构建许多 dom 节点,在首次渲染时会卡顿,而且如果页面发生回流,过多的 dom 节点也会造成交互上的问题,众所周知,原生 JS 操作并不慢,框架优化的是渲染的效率,所以会导致 JS 计算完成,但是页面不可交互的问题

优化的思路就是只渲染用户可视区域内的部分,其他部分暂时不渲染,先看代码,这里以 Vue 举例

<template>
  <div class="container" @scroll="onScroll">
    <div ref="panel">
      <div v-for="item in showList" class="item" :key="item">
        {{ item }}
      </div>
    </div>
  </div>
</template>

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

const list = reactive(new Array(1000000).fill(0).map((v, i) => `item-${i}`))
const start = ref(0)
const end = ref(10)
const totalHeight = ref(list.length * 1000000)
const panel = ref(null)
const count = 10
const showList = computed(() => list.slice(start.value, end.value))
onMounted(() => {
  panel.value.style.height = totalHeight.value + 'px'
  end.value = count
})
const onScroll = (e) => {
  start.value = Math.floor(e.target.scrollTop / 60)
  end.value = start.value + count
  panel.value.style.paddingTop = start.value * 60 + 'px'
}
</script>

<style scoped>
.container {
  height: 600px;
  overflow: scroll;
}

.item {
  height: 60px;
}
</style>

我们的思路是用 2 个指针来决定数组的哪一段被进行渲染,也就是 showList,count 为我们想要一次给用户展现几条数据,当发生滚动时,我们拿到 scrollTop,它代表滚动条卷去的高度,初始为 0,用它可以判断 start 指针应该在的位置,然后我们就可以确定 end 指针的位置,从而每次滚动时修改 showList,从而控制数据量

使用 paddingTop 来代表滚动条卷去的高度,假如我们滚动到第三条数据,其实此时前两条数据的位置是没有数据的,只有我们设置的 paddingTop,这是核心

看上去滚动也很丝滑,但是还有一些问题,比如滚动过快导致多次渲染和短暂的白屏,我们可以为滚动事件做防抖处理,因为如果用户快速滚动的话,也是想得到最后的结果,而短暂的白屏的问题,可以对数据进行预加载

我们之前的思路是每次只选数组的 10 条数据来加载,这次我们扩大范围,改成正负 10 以内,代码如下

<template>
  <div class="container" @scroll="onScroll">
    <div ref="panel">
      <div v-for="item in showList" class="item" :key="item">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script setup>
import {computed, onMounted, reactive, ref} from "vue";
const list = reactive(new Array(10000).fill(0).map((v, i) => `item-${i}`))
const start = ref(0)
const end = ref(1)
const count = 10
const totalHeight = ref(list.length * 1000000)
const panel = ref(null)
const buffTop = 10, buffBottom = 10;  //新增范围
const showList = computed(() => list.slice(start.value, end.value))
onMounted(() => {
  panel.value.style.height = totalHeight.value + 'px'
  end.value = count + buffBottom
})
const onScroll = (e) => {
  let startValue = Math.floor(e.target.scrollTop / 60)
  start.value = startValue > buffTop ? startValue-buffTop: startValue
  // 如果刚刚开始滚动,那么开始指针就是卷去的元素的下标,
  如果已经滚动了超过 10 个元素,
  用当前元素下标减去 10,得到用户可视区域的元素的上 10 个元素的下标,
  也就是得到了开始指针的位置
  end.value = start.value + count + buffBottom
  panel.value.style.paddingTop = start.value * 60 + 'px'
}
</script>

<style scoped>
.container {
  height: 600px;
  overflow: scroll;
}

.item {
  height: 60px;
}
</style>

这样一来,我们一次性渲染超出用户可视区域能盛放的节点,在下拉时,也就是 dom diff 的时候,vue 会复用之前的 dom 节点,测试时观察元素的渲染情况,滚动速度一般时会有大量的节点复用,而且用户即将看见的节点早已加载好了 10 个,所以体验会更好

总结:

  1. 长列表优化,充分使用 paddingTop 来把元素向下挤压,而不是直接渲染
  2. 提高加载范围,也就是预加载,能有更流畅的体验