当前位置: 首页 > Web前端 > HTML

探索表格组件虚拟化

时间:2023-03-28 10:28:33 HTML

豆皮粉,我们又见面了。今天一期,字节跳动数据平台峰峰将带大家一起探讨Table组件虚拟化的原理和解决方案。本文作者:龚锋前言列表和表格的虚拟优化并不是一个新话题。最近,团队发现业界并没有相对通用的表虚拟化解决方案。这是为什么?如何解决?本文将逐步介绍团队内部在React+AntDesign技术栈下Table组件虚拟化的不同实践思路,并分析可能遇到的困难。虚拟化问题说明在列表页、瀑布流、Select组件中,我们可能会遇到大数量级列表渲染的场景。过去我们总结了一套行之有效的方法:对于列表,我们通过计算保证每次滚动窗口时只渲染部分元素。这样既减轻了首屏的压力,又保证了长时间加载时不会再有性能负担,可以满足上述大部分场景的高性能优化需求。具体来说,我们使用已知的固定行高和滚动偏移量来计算要滚动到的表格行索引,只渲染有限窗口中需要的元素,并相应地设置列表。简要过程如下:计算当前可见区起始数据的startIndex计算当前可见区结束数据的endIndex计算当前可见区数据并渲染到页面计算偏移位置startOffsetstartIndex对应的数据在整个列表中的偏移位置并设置到列表中计算endIndex对应的数据相对于可滚动区域底部的偏移位置endOffset并设置到列表中的方案被大多数开发者广泛讨论.推荐参考ShallowTalkVirtualList的实现原理,里面对问题的思考和实现有比较详细的描述。现在,我们的场景来到了AntDesign的Table组件。我们面临一个很相似的问题:在业务中,当Table涉及到1000+行&100+列的渲染时,由于cells也有一定的复杂逻辑,页面渲染时间往往需要5000ms左右才能卡住。这显然是不能接受的。其实从一长串列表到一个Table组件,我们的列表无非是从一维的轴上升到二维的平面。因此,所谓的表格虚拟化无非是希望表格能够实现:在兼容表格现有功能的情况下,表格只渲染窗口平面的内容,隐藏行和窗外的柱子。整理现状AntDesign我们内部的React+umi+AntDesign技术栈是前端行业比较常见的架构基础。AntD是业界通用的React组件库。它提供的Table组件可以很方便的帮我们解决很多常见的需求,包括但不限于:行选择、行展开、行过滤、数据分页、列固定……目前AntDesign有AntD@3.26和AntD@4.11两大类版本,后者进行了一定程度的重构和优化,是官方推荐的最新内容。AntDesign3文档中删除了支持虚拟化的demo(其实也可以用),在AntDesign4文档中可以看到官方建议用户通过访问react-window来解决虚拟化表的问题。从官方的Demo来看,AntD提供了一个components属性,通过传入一个对象,在其body属性中,给出了一个ReactWindow提供的虚拟化组件,满足要求...//VariableSizeGrid是react提供的组件-windowconstrenderVirtualList=(rawData,{scrollbarSize,ref,onScroll})=>......ReactWindowreact-window是一个流行的表虚拟化开源库,它产生具有不同特性的虚拟化组件,所以用户可以专注于解决不同虚拟场景中的问题。react-window的原理并不复杂。主要是本文虚拟化问题描述中虚拟化要求内容的实现。主要是通过监听onScroll,动态调整表格的横轴偏移量,选取适量的部分数据,渲染可见区域的内容。他的前身是反应虚拟化的。经过重构升级,作者针对table和list两种不同的场景做了更好的抽象。通过复用公共部分的逻辑,实现了更好的性能,代码打包大小减少到原来的20%。接入过程中遇到的问题AntD好像给出了一套虚拟化方案。但是稍微深入调查一下,不难发现在Github的Issue中,用户对官方推荐方案的反馈并不理想。例如:issue#21022,使用AntD3后,扩展、多选等功能缺失。如issue#20339中所述,使用AntD4后,表功能点大部分丢失。针对以上问题,官方回复为用户自行解决。因此:根据AntD文档,使用虚拟化进行配置。虽然一定程度上可以实现虚拟化,但是会造成很多表功能的丢失。这是我们迫切需要解决的主要问题。rc-table由于官方并没有给出很好的虚拟化方案,我们只好深入了解AntD的内部结构,找到问题的出发点。看完代码我们可以了解到,4.11中AntD/Table的代码结构简单描述如下:在AntDesign/Table中:初始化表格的大小和行列的内容,初步整理事件和数据排序、分页、过滤等功能计算数据和列的逻辑调用rc-table依赖于rc-table:注册各种行列单元格事件,完成列固定、行扩展、渲染等各种样式需求。至此,我们可以理解为AntD架构本身在一定程度上实际上实现了表逻辑。数据的划分,与数据顺序和内容无关的逻辑,已经单独抽象到rc-table库中。总的来说,我们可以理解为下图:我们也要想清楚:这三层逻辑,到底是应该分别保留,改造,还是替换?如果保留,引入虚拟化逻辑后如何解决虚拟化逻辑对现有框架的影响?如果不保留,如何选择新的框架?群内方案展示项目组针对表虚拟化工作产生了多种不同的思路:方案一:基于rc-table实现虚拟化,不依赖react-window和AntD实现思路:在不同的业务中,我们需要的表组件功能点不一致。在极端场景下,我们可能只需要使用少量的AntD特性,定制化程度高。因此,我们可以考虑放弃使用AntD,直接使用rc-table,并进行一定的修改。实现方法:fork出一个稳定版的rc-table,放到代码库中,根据虚拟化的原理,完成对rc-table的必要改造。自己实现排序、选择、定列等上层功能。方案优缺点:优点:不需要自己实现基本的表格展示,这部分由rc-table来完成,我们只需要对滚动事件做一个小的修改,进行虚拟化。外部函数特性方便定制。缺点:失去了AntD的功能基础。方案二:拦截AntD中的展示数据,手动销毁表格外的dom,手动创建占位符。实现思路:当业务对AntD的各种功能依赖较大时,不能直接去掉AntD。只能保留整体框架,寻找局部改造的切入点。所以修改了antD中的table滚动事件,使用了新的onScroll逻辑:判断dom行的位置,计算indexsetState动作,手动销毁超出窗口的dom内容,创建等量的空白区域保持滚动条位置的高度。剪切和更新数据内容,保证窗口数据的正确性。在这个逻辑下,只修改AntD导入的数据内容,其余操作基于jsdom手动完成,影响小,完成度高。但是对个别表功能点的影响需要仔细测试。方案优缺点:优点:前向兼容AntdTable配置参数,只需要增加几个props,改造成本极低,直接操作dom。除了数据拦截,大部分开发工作不依赖AntD/rcTable代码,性能有保障。缺点:需要谨慎处理修改方案对列固定、行扩展等特性的影响。由于无法提前获取行高,所以暂时不支持直接定位。等效的解决方案是先搜索对应的数据,然后将结果加载到InfinityTable中。放弃antd。方案三保留AntD的props定义,保证从AntD迁移的方便,然后使用react-table完成大部分功能。虚拟化部分使用了react-window,底层开发了一个新的具有虚拟化功能的基础表。框架选择理由本方案介绍github上火热的开源reacthook框架react-table,是一个数据逻辑hook框架。因为排序、选择等数据逻辑是相似的,所以不需要重写,有利于节省数据处理的成本,又不干扰UI和虚拟化工作。方案优缺点:优点:改造深入,重构程度高,方便后续任何扩展,方便性能优化使用开源产品实现原有AntD的开发内容,节省一定成本缺点:基础表实现成本高,基础表需要结合react-table的api重新实现,遇到各种坑需要自己踩和修改应用虚拟化后,滚动太快action会导致placeholder不更新,只能看到空白内容,需要等待一小段时间再加载。当表格不断快速滚动时,空白内容不断闪烁。问题解决:目前没有很好的解决办法。增加预加载区域的大小和优化单元格的渲染内容可以降低问题的严重性。有同学提出通过监控滚动速度来动态调整preload区域的大小,这是不现实的想法。单元格自适应环绕问题陈述:当单元格文本内容较长时,希望高度可以自适应。乍一看,这个需求似乎可以通过使用react-window的可变高度组件来解决,但实际上,组件需要提供一个高度函数://Returnsaiteminthedirectionbeingwindowed.//对于垂直列表,这是行高。//对于水平列表,这是列width.itemSize:(index:number)=>number当文字变化时,我们还需要渲染得到每行的高度,所以这个api并没有想象中的漂亮。这个问题还是要通过重新渲染,通过ref获取dom节点来解决,但是这样会拖慢性能,所以经过考虑,不支持这个方案。排版而不是nativetable,为了实现固定列,我们需要使用三个表来固定效果,所以在滚动的时候,我们需要同时改变多个表的scrollTop和它们的数据拦截。问题解决可以使用一个状态来同步多个表之间的scrollTop,但是这种实现方式可能会因为性能原因造成表渲染的顺序,进而可能会导致三个表错位的问题。(AntD的header和body对齐本身也有这个问题。)AntD不区分三表,而是利用原生的tr/td和css在一个表的sticky特性,部分避免了这个问题,但是并没有很好的解决.在存在fixedcolumn的场景下,header和body的同步还是有类似的问题。你可以把三个表变成一个表,然后用性能最好的3Dtransform来解决这个问题。下面推荐的开源产品rsuite-table通过这个方案更好的解决了这个问题。列虚拟化问题描述:由于目前的方案主要针对行数据量大的场景,所以在遇到表列数据量大的场景时,仍然存在性能问题。如果引入react-window的Grid组件进行列虚拟化,需要兼容分列、固定列等功能,实际实现起来比想象的要复杂。解决问题:列数过多,产品设计不合理。建议从产品层面改善数据显示问题。目前不支持列虚拟化。开源表单组件推荐由于内部资源无法对外共享,所以没有办法对外发布上述方案的产品。如果你想使用带有虚拟化功能的表,但又不想做任何开发工作,这里有一些不错的开源产品rsuite-tablehttp://rsuite.github.io/rsuite...虚拟化和表性能优秀,大部分功能齐全表头功能较弱,无列过滤,无拖拽改变列宽react-base-tablehttps://github.com/Autodesk/r...自带虚拟化,基本完备功能,其他功能与AntD相比有一定的差距。总结表组件虚拟化原理比较简单。即使没有react-window的帮助,我们看到每个同学都可以用自己的想法实现类似的功能需求。之所以有这么多方案无法统一,主要是因为不同的项目对表库的要求不同:有的项目要求成本低,有的要求兼容各种特性,有的项目历史包袱重。而一个新的、全面的表库会存在更换成本和稳定性风险等问题。因此,因地制宜改造自己的项目,实现一套适合自己项目的虚拟化解决方案,是最快最容易被大家接受的。因此,本文从三个不同的场景出发,总结了视野范围内解决这个问题的不同思路。对于想要开箱即用的同学,也给出了几款经过横向比较值得推荐的开源产品。所谓授人以鱼,不如授人以渔。希望本文所讨论的内容或多或少都朝着提高表性能和表虚拟化的方向发展,能够给读者一些启发,为字节类产品的用户带来更快、更高效的数据。不错的体验。结束