性能优化之虚拟列表及白屏原因浅析写在最前 看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出
写在最前
引出
作为一名前端工程师,大数据量的展示是我们日常的面临的挑战之一
大数据量展示的场景当中我们碰到比较多的就是长列表
一般我们处理长列表在大数据量下的展示就3种方案
- 分页
- 懒加载(滚动加载或者说无限滚动)
- 虚拟列表
我们今天要说的就是虚拟列表
什么是虚拟列表?
引出痛点
首先不使用没有虚拟列表,我们用React先渲染10w条数据
统计一下时间
用虚拟列表来渲染
统计一下时间
对比一下二者的时间,很明显虚拟列表,有着很明显的优势。
Ps:这里每个item还只是简单的结构,随着复杂度的增加,优势应该会更加明显。
虚拟列表的理解
下面看一张原理图
再结合滚动时候的样子
虚拟列表的意思就呼之欲出了,就是只渲染指定视口内的可见部分,其余部分以虚拟(内存)的形式保存,等待需要时再渲染。
来实现一个简单的虚拟列表
虚拟列表有好多种,大致上有2种,定高,不定高的
这里只讲定高的。目的在于理解最简单的原理
定义一下dom结构
窗口的div包裹,设置overflow:auto
和 监听滚动事件
内部内容区域利用样式margin-top
和height
来撑起整个高度,使得窗口div容器能顺利滚动
然后理一下组件的props
interface ListProps {
data:any[] // 渲染的数据
itemHeight?:number // 每个item的高度
containerHeight?:number // 外部窗口的高度
bufferIndex?:number // 缓冲数量(这里暂不实现)
}
最后写一下代码
// VirtualList.tsx
const VirtualList: React.FC<ListProps> = ({data,itemHeight:inputHeight,containerHeight:cHeight}) => {
const [scrollTop, setScrollTop] = useState(0);
const handleScroll = (event:React.UIEvent<HTMLDivElement>) => {
setScrollTop(event.currentTarget.scrollTop);
};
const itemHeight = inputHeight ? inputHeight : 50;
const containerHeight = cHeight ? cHeight :window.innerHeight
const totalItems = data.length;
const visibleItems = Math.floor(containerHeight / itemHeight);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleItems;
const visibleData = data.slice(startIndex, endIndex);
return (
<div
style={{ height: `${visibleItems * itemHeight}px`,overflowY: "auto" }}
onScroll={handleScroll}
>
<div style={{ height: `${totalItems * itemHeight - startIndex*itemHeight}px`,marginTop:`${startIndex*itemHeight}px` }}>
{visibleData.map((item, index) => (
<div key={index} style={{ height: `${itemHeight}px`,backgroundColor: isOdd(index)?"lightpink":"lightgray"}}>
{item.content}
</div>
))}
</div>
</div>
);
};
ok,到这里一个最简单的虚拟列表就实现完毕了~
那么有什么问题呢?
数据量大时,当我们快速去拖动滚动条,会出现滚动条拖动不卡顿,但列表的显示短暂白屏的问题
加入缓冲区
意思是除了窗口区域之外,多渲染一些,具体的逻辑可以去参考react-window
可以看到加入缓冲区之后,白屏的效果得到了缓解
其实不难发现,还是会有白屏的问题。
还有1个优化方法
参考骨架屏的思路,提前将不显示的区域全部渲染出骨架,避免白屏效果
白屏原因分析
在这里说下我理解的白屏原因
首先梳理一下用户触发滚动的过程:
-
用户的滚动从browser process经由render 进程的合成器线程Compositor Thread转发给渲染主线程
-
渲染主线程Main Thread触发相关的用户输入事件处理加入到宏任务队列当中
-
然后在事件循环的每个迭代当中,渲染主线程会取出1个宏任务,压入执行栈给v8引擎去执行,之后依次清空执行过程当中产生的微任务队列
-
接着主线程会进行
页面样式的重新计算
、页面布局的重新计算
-
交给合成器线程Compositor Thread
分层绘制
、合成、生成图层
和分块 -
最后将这些图块交给gpu线程去绘制到屏幕上
过程如下图
可以看到 合成器线程 控制着 屏幕刷新率事件、 用户的输入,和frame的输出
在这里推荐Chrome高级工程师Paul Lewis大佬写的The Anatomy of a Frame
里面有当提到滚动的时候,合成器线程尝试直接转换成屏幕的移动。通过更新图层位置和直接传递帧给gpu通过gpu线程
但如果有事件处理器的话,还是需要主线程去干活,那么为啥滚动的时候流畅,只是白屏呢?
我们可以看看Chromium的设计文档
里面有一篇文章GPU Accelerated Compositing in Chrome 介绍了GPU加速合成机制,里面明确提到了对滚动性能的优化
输入和输出都在合成器线程的控制下,合成器线程可以保证用户输入的视觉响应。
touch事件的滚动可以被js当中的preventDefault()取消,而滚动事件不行。
因为滚动事件会异步传递给JavaScript,合成器线程可以立即开始滚动,而不管主线程是否立即处理滚动事件。
想必看到这里,白屏的原因昭然若揭:
合成器线程控制了用户的输入,以及图像的输出权,所以有优先响应用户输入(比如滚动)的能力,它可以先响应滚动,而无需等待渲染主线程去调用v8执行事件处理函数及后续的操作的执行
在项目当中使用虚拟列表
生产环境下一般要考虑健壮性强的库,在这里推荐一些 Vue: vueuse vue-virtual-scroller React: ahooks react-window
挖坑
-
不定高的虚拟列表实现
-
实现一个虚拟列表的hooks(支持定高和不定高)
写在最后
如果你觉得本文对你有所帮助,不妨给我一个点赞和收藏,这将是对我最大的鼓励。
转载自:https://juejin.cn/post/7254786711719608380