使用 requestAnimationFrame 异步分片渲染大数据表格

1,144 阅读3分钟

背景

表格一页展示数据太多,后端数据返回了,但是渲染到页面的速度很慢,用户等待时间太长,体验差;

原因

dom 节点太多,一次性渲染全部节点压力太大;

解决方法

模拟后端返回1w条数据

<template>
  <div>
    <t-card>
      <t-button @click="handleQuery">直接请求10000条数据</t-button>
      <t-button class="ml-6" @click="handleQueryForSlice">切片渲染</t-button>
      <t-button class="ml-6" @click="reset">清空</t-button>

      <t-table
        class="mt-6"
        row-key="index"
        stripe
        hover
        :data="data"
        :columns="columns"
        cell-empty-content="-"
        lazy-load
      >
      </t-table>
    </t-card>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, nextTick } from 'vue'

const columns = [
  { colKey: 'index', title: '索引' },
  { colKey: 'txt', title: '数据' },
  { colKey: 'xx', title: '测试' },
  { colKey: 'xx2', title: '测试2' },
  { colKey: 'xx3', title: '测试3' },
  { colKey: 'xx4', title: '测试4' },
  { colKey: 'xx5', title: '测试5' },
  { colKey: 'xx6', title: '测试6' },
  { colKey: 'xx7', title: '测试7' },
  { colKey: 'xx8', title: '测试8' },
  { colKey: 'xx9', title: '测试9' },
  { colKey: 'xx10', title: '测试10' },
  { colKey: 'xx11', title: '测试11' }
]

const data = ref([])

const mockData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        Array.from({ length: 10000 }, (_, index) => ({
          index: index + 1,
          txt: `数据${index + 1}`,
          xx: `测试${index + 1}`,
          xx2: `测试${index + 1}`,
          xx3: `测试${index + 1}`,
          xx4: `测试${index + 1}`,
          xx5: `测试${index + 1}`,
          xx6: `测试${index + 1}`,
          xx7: `测试${index + 1}`,
          xx8: `测试${index + 1}`,
          xx9: `测试${index + 1}`,
          xx10: `测试${index + 1}`,
          xx11: `测试${index + 1}`
        }))
      )
    }, 2000)
  })
}

const handleQuery = async () => {
  const res = await mockData()
  console.log(res)
  data.value = res
}

const handleQueryForSlice = async () => {
  const res = await mockData()
  console.log(res)
  await setTableData(res)
}

const setTableData = (input: unknown[]) => {
  if (!input.length) {
    return
  }

  requestAnimationFrame(async () => {
    const num = 2000
    data.value.push(...input.slice(0, num))
    setTableData(input.slice(num))
  })
}

const reset = () => {
  data.value = []
}
</script>

<style lang="less" scoped></style>

页面详情

image.png

直接渲染10000条

12.gif 数据已经返回了,但是渲染到页面上还需要等待一段时间;

切片渲染

123.gif 切成 1/5,分5次去渲染,用户等待的时间更短一些,但是页面还是不断在插入数据,当每次插入的数据越少时,所用的帧数就越多,实际完成全部数据渲染的时间更长;

切片渲染的原理其实是使用到了requestAnimationFrame 这个API,将表格数据赋值给响应式数据这一步,放到了raf中去实现,这实际上是一个异步操作,requestAnimationFrame 用于在下一个重绘之前执行回调函数。这意味着它不会立即执行你传递的回调函数,而是会在浏览器下一次准备更新屏幕显示内容时调用它。这样可以确保动画的平滑性和性能,因为它使你的动画帧与浏览器的刷新率同步。

因为 requestAnimationFrame 是异步的,它不会阻塞主线程。它会在浏览器空闲时执行回调,因此它比 setTimeout 或 setInterval 更高效,适用于动画和需要高性能的任务。

结论

切片渲染是否算是一种优化呢,在项目实际中看来,这看起来更像是假优化,在数据拿到的一瞬间,就将一部分数据渲染出来,但如果使用不合理,切分的数量不合适,会导致页面拿到全部数据的时间变长,且滚动条也会一直在变长,拖动的时候就会感觉到很卡,这部分用户体验也是比较差的。

如果结合虚拟列表使用,估计会有不错的效果;但实际开发中,大数据表格都会采用分页,在字段不多,自定义单元格内容较少的情况下,也不需要做其他额外优化了;