瀑布流使用虚拟列表性能优化
- 2022 年 11 月 15 日
- 笔记
- javascript, 前端
瀑布流算是比较常见的布局了,一个般常见纵向瀑布流的交互,当我们滚动到底的时候加载下一页的数据追加到上去。因为一次加载的数据量不是很多,页面操作是也不会有太大的性能消耗。但是如果当你一直往下滚动加载,加载几十页的时候,就会开始感觉不那么流畅的,这是因为虽然每次操作的很少,但是页面的 DOM 越来越多,内存占用也会增大,而且发生重排重绘时候浏览器计算量耗时也会增大,就导致了慢慢不能那么流畅了。这个时候可以选择结合虚拟列表方式使用,虚拟列表本身就是用来解决超长列表时的处理方案。
瀑布流
瀑布流的实现方式有很多种,大体分为:
- CSS: CSS 实现的有 multi-column、grid ,CSS 实现存在一定局限性,例如无法调整顺序,当元素高度差异较大时候不是很好处理各列间隔差等。
- JavaScript:JavaScript 实现的有 JavaScript + flex、JavaScript + position,JavaScript 实现兼容性较好,可控制性高。
因为我的瀑布流是可提前计算元素宽高,列数是动态的,所以采用了 JavaScript + position 来配合 虚拟列表 进行优化。
js + flex 实现
如果你的瀑布流 列是固定,列宽不固定 的,使用 flex 是个很好选择,当你的容器宽度变话时候,每一列宽度会自适应,大致实现方式
将你的数据分为对应列数
let data1 = [], //第一列
data2 = [], //第二列
data3 = [], //第三列
i = 0;
while (i < data.length) {
data1.push(data[i++]);
if (i < data.length) {
data2.push(data[i++]);
}
if (i < data.length) {
data3.push(data[i++]);
}
}
然后将你的每列数据插入进去就可以了,设置 list 为 flex 容器,并设置主轴方向为 row
<div class="list">
<!-- 第一列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
<!-- 第二列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
<!-- 第三列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
</div>
js + position 实现
这种方式比较适合 列定宽,列数量不固定情况,而且最好能计算出每个元素的大小。
大致 HTML 结构如下:
<ul class="list">
<li class="list-item"></li>
<!-- more items-->
</ui>
<style>
.list {
position: relative;
}
.list-item {
position: absolute;
top: 0;
left: 0;
}
</style>
JavaScript 部分,首先需要获取 list 宽度,根据 list.width/列宽 计算出列的数量,然后根据列数量去分组数据和计算位置
// 以列宽为300 间隔为20 为例
let catchColumn = (Math.max(parseInt((dom.clientWidth + 20) / (300 + 20)), 1))
const toTwoDimensionalArray = (count) => {
let list = []
for (let index = 0; index < count; index++) {
list.push([])
}
return list;
}
const minValIndex = (arr = []) => {
let val = Math.min(...arr);
return arr.findIndex(i => i === val)
}
// 缓存累计高度
let sumHeight = toTwoDimensionalArray(catchColumn)
data.forEach(item => {
// 获取累计高度最小那列
const minIndex = minValIndex(sumHeight)
let width = 0 // 这里宽高更具需求计算出来
let height = 0
item._top = minIndex * (300 + 20) // 缓存位置信息,后面会用到
item.style = {
width: width + 'px',
height: height + 'px',
// 计算偏移位置
transform: `translate(${minIndex * (300 + 20)}px, ${sumHeight[minIndex]}px)`
}
sumHeight[minIndex] = sumHeight[minIndex] + height + 20
})
动态列数
可以使用 ResizeObserver(现代浏览器兼容比较好了) 监听容器元素大小变化,当宽度变化时重新计算列数量,当列数量发生变化时重新计算每项的位置信息。
const observer = debounce((e) => {
const column = updateVisibleContainerInfo(visibleContainer)
if (column !== catchColumn) {
catchColumn = column
// 重新计算
this.resetLayout()
}
}, 300)
const resizeObserver = new ResizeObserver(e => observer(e));
// 开始监听
resizeObserver.observe(dom);
过渡动画
当列数量发生变化时候,元素项的位置很多都会发生变化,如下图,第 4 项的位置从第 3 列变到了第 4 项,如果不做处理会显得比较僵硬。
好在我们使用了 transform(也是为什么使用 top、left 原因,transform 动画性能更高) 进行位置偏移,可以直接使用 transition 过渡。
.list-item {
position: absolute;
top: 0;
left: 0;
transition: transform .5s ease-in-out;
}
使用虚拟列表
瀑布流存在的问题
很多虚拟列表的都是使用的单列定高使用方式,但是瀑布流使用虚拟列表方式有点不同,瀑布流存在多列且时是错位的。所以常规 length*height 为列表总高度,根据 scrollTop/height 来确定下标方式就行不通了,这个时候高度需要根据瀑布流高度动态决定了,可显示元素也不能通过 starindex-endindex 去截取显示了。
如下图:蓝色框的元素是不应该显示的,只有与可视区域存在交叉的元素才应该显示
可视元素判定
先来看下面图,当元素完全不在可视区域时候就视为当前元素不需要显示,只有与可视区域存在交叉或被包含时候视为需要显示。
因为上面瀑布流的实现采用的是 position 定位的,所以我们完全能知道所有元素距离顶部的距离,很容易计算出与可视区域交叉位置。
元素偏移位置 < 滚动高度+可视区域高度 && 元素偏移位置 + 元素高度 > 滚动高度
如果只渲染可视区域范围,滚动时候会存在白屏再出现,可视适当的扩大渲染区域,例如把上一屏和下一屏都算进来,进行预先渲染。
const top = scrollTop - clientHeight
const bottom = scrollTop + clientHeight * 2
const visibleList = data.filter(item => item._top + item.height > top && item._top < bottom)
然后通过监听滚动事件,根据滚动位置去处理筛选数。这里会存在一个隐藏性能问题,当滚动加载数据比较多的时候,滚动事件触发也是比较快的,每一次都进行一次遍历,也是比较消耗性能的。可以适当控制一下事件触发频率,当然这也只是治标不治本,归根倒是查询显示元素方法问题。
标记下标
应为列表数据的 _top 值是从小到大正序的,所以我们可以标记在可视区元素的下标,当发生滚动的时候,我们直接从标记下标开始查找,根据滚动分几种情况来判断。
1> 如果滚动后,标记下标元素还在可视范围内,可以直接从标记下标二分查找,往上往下找直到不符合条件就停止。
2> 如果滚动后,标记下标元素不在可视范围内,根据滚动方向往上或者往下去查找,然后更新下标值。这个时候存在一种情况,就是当用户拖动滚动条滚动幅度特别大的时候,可以将下标往上或者往下偏移,偏移量根据 滚动高度/预估平均高度*列数 去估算一个,然后在跟新这个预估下标进行查找。
抖动问题
我们 absolute 定位会撑开容器高度,但是滚动时候还是会存在抖动问题,我们可以自定义一个元素高度去撑开,这个元素高度也就是我们之前计算的每一列累计高度 sumHeight 中最大的那个了。
过渡动画问题
当列宽发生变化时,元素位置发生了变化,在可视区域的元素也发生了变化,有些元素可能之前并没有渲染,所以使用上面 CSS 会存在新出现元素不会产生过渡动画。好在我们能够很清楚的知道元素原位置信息和新的位置信息,我们可以利用 FLIP 来处理这动画,很容易控制元素过渡变化,如果有些元素之前不存在,就没有原位置信息,我们可以在可视范围内给他随机生成一个位置进行过渡,保证每一个元素都有个过渡效果避免僵硬。
总结
上面情况仅仅是针对动态列数量,又能计算出高度情况下优化,可能业务中也是可能存在每项高度是动态的,这个时候可以采用预估元素高度在渲染后缓存大小位置等信息,或者离屏渲染等方案解决做出进一步的优化处理。