avatar

🧊foril

avatar

🧊foril

长列表优化:虚拟列表

2024-07-13 -

什么是虚拟列表

在有些需求场景中,我们可能需要渲染大量列表项,同时分页加载可能不是理想的加载方式。
大量的项目 layout 会导致页面性能下降,因为浏览器需要计算每个元素的位置,这个计算是非常昂贵的。同时,大量的 DOM 元素也会占用大量的内存,导致页面加载变慢。

这时候就需要用到长列表优化技术以优化渲染开销,虚拟列表就是其中一种。

虚拟列表是一种只渲染可视区域的列表,当用户滚动列表时,只渲染可视区域(也可以包含即将可视的缓冲区)的列表项,这样可以大大减少渲染开销。每当用户滚动列表时,虚拟列表会根据滚动位置动态调整列表项显示的内容,对用户来说就像是正常滚到了下一个项目,但实际上是列表向下平移且显示的内容换了一批。

下面这个 demo 可以展示虚拟列表的效果:左边是虚拟列表,右边是真实列表,当关闭动画之后,左右两边的列表对用户来说体验是一样的。而打开动画之后,可以看到每当用户滚动列表的高度超过一个列表项之后,页面实际上渲染的仍然是同一批 DOM 元素,只是根据滚动位置动态调整了显示的内容以及位置。

第三个列表项的样式不同,可以更好的理解虚拟列表的原理

更改已经渲染在页面的 DOM 元素的内容和样式相比于创建新的 DOM 元素要快得多,因为浏览器不需要重新计算元素的位置,这就是虚拟列表的优势所在。

下面分享一个简易虚拟列表的 Vue 代码及其注释。简单来说就是通过监听滚动事件,根据滚动位置动态调整列表项显示的内容,以及位置。

<template> <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }" ></div> <!-- infinite-list-phantom 作为容器,高度始终是最大高度,用于提供滚动条 --> <!-- 内部 list 的定位相对于容器, 距离 top 为 0 保证 transform 有效 --> <!-- list 一共只有常数个项目,保证屏幕显示完全即可--> <!-- scroll 事件发生时,容器正常滚动, 而 list 则计算应该平移的高度并平移 --> <!-- 一旦第一个 item 滚出屏幕,就会让 list 向下平移 item 的高度,同时会更新显示的内容 --> <!-- 这样对用户来说就像是正常滚到了下一个项目,但实际上是列表向下平移且显示的内容换了一批 --> <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> <script> export default { name: "VirtualList", props: { //所有列表数据 listData: { type: Array, default: () => [], }, //每项高度 itemSize: { type: Number, default: 200, }, }, computed: { //列表总高度 listHeight() { return this.listData.length * this.itemSize; }, //可显示的列表项数 visibleCount() { return Math.ceil(this.screenHeight / this.itemSize); }, //偏移量对应的style getTransform() { return `translate3d(0,${this.startOffset}px,0)`; }, //获取真实显示列表数据 visibleData() { return this.listData.slice( this.start, Math.min(this.end, this.listData.length) ); }, }, mounted() { this.screenHeight = this.$el.clientHeight; this.start = 0; this.end = this.start + this.visibleCount; }, data() { return { //可视区域高度 screenHeight: 0, //偏移量 startOffset: 0, //起始索引 start: 0, //结束索引 end: null, }; }, watch: { startOffset() { console.log(this.startOffset); }, }, methods: { scrollEvent(e) { e.preventDefault(); //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); }, }, }; </script> <style scoped> .infinite-list-container { height: 100%; overflow: auto; position: relative; -webkit-overflow-scrolling: touch; } .infinite-list-phantom { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } .infinite-list { left: 0; right: 0; top: 0; position: absolute; text-align: center; } .infinite-list-item { padding: 10px; color: #555; box-sizing: border-box; border-bottom: 1px solid #999; } </style>

Pros & Cons

好处

  1. 性能提升

    • 减少DOM操作:虚拟列表只渲染用户视口内的部分数据,大大减少了需要处理的 DOM 元素数量。
    • 内存占用低:只需在内存中保留可见的列表项,减少了浏览器的内存消耗。
  2. 提高响应速度

    • 快速滚动:由于只渲染可见部分,滚动时不需要重新计算和渲染整个列表,用户体验更加流畅。
    • 即时响应:用户操作(如滚动、点击)可以更快地得到响应。
  3. 节省带宽

    • 按需加载数据:结合懒加载技术,可以仅在需要时加载数据,减少了初始加载时间和带宽消耗。
  4. 可扩展性强

    • 适用于大数据集:特别适用于那些数据量很大且不能一次性加载到页面上的情况,例如社交媒体时间线、电子商务商品列表等。

坏处

  1. 实现复杂

    • 开发难度:实现虚拟列表需要更多的逻辑处理,包括计算可见区域、管理数据池、处理滚动事件等。
    • 调试困难:由于渲染是动态的,调试和排查问题可能会更加复杂。
  2. 浏览器兼容性问题

    • 旧浏览器支持:一些旧版本的浏览器可能不完全支持所需的 CSS 属性(如 transformwill-change)和 JavaScript 特性,导致兼容性问题。
  3. SEO问题

    • 搜索引擎抓取:对于需要 SEO 优化的页面,虚拟列表中的内容可能不会被搜索引擎完全抓取到,因为并不是所有的内容都在初始加载时渲染出来。
  4. 用户体验

    • 感知延迟:如果懒加载实现不当,用户可能会在快速滚动时看到加载延迟,影响用户体验。
    • 无效的链接和交互:在某些情况下,用户可能无法使用浏览器的内置查找功能(如 Ctrl+F)查找未加载的内容,或者在跳转特定项目时遇到问题。
  5. 代码复杂度增加

    • 维护难度:虚拟列表引入的复杂逻辑和优化措施可能使代码变得更加复杂,增加了维护的难度和成本。