
在小程序开发场景中,列表是最基础且使用频率最高的组件,当列表数据量达到数百条及以上时,原生普通滚动列表会出现肉眼可见的性能问题,这也是虚拟列表方案诞生的核心原因。想要弄懂虚拟列表,首先需要明确原生列表卡顿、加载慢、滑动掉帧的底层根源。
小程序视图层基于原生渲染引擎搭建,每一条列表项都会对应独立的DOM节点,同时附带样式、绑定事件、内部子节点等附属渲染资源。原生列表的渲染逻辑为:一次性渲染全部数据对应的DOM节点,不会随着滚动动态增减节点。即便部分列表项已经滚动到屏幕可视区域之外,脱离用户视野,对应的DOM节点依旧会常驻视图层,持续占用内存与渲染线程资源。
具体性能损耗分为三个维度:第一是内存占用,海量DOM节点会持续占用视图层内存,页面停留时间越长,内存堆积越严重,极易触发小程序内存回收机制,导致页面闪退、白屏;第二是首屏渲染耗时,页面初始化时需要遍历全部数据、创建全部节点、计算全部节点布局,数据量越大,首屏白屏加载时间越长;第三是滚动流畅度问题,滑动页面时,渲染线程需要持续监听全部节点的位置变化,进行重排与重绘,大量节点会直接拉高主线程压力,出现滑动卡顿、触摸延迟、滚动惯性失效等问题。
行业内常规的分页加载、懒加载方案只能缓解部分首屏压力,无法解决可视区外冗余DOM节点常驻的问题,只要数据持续累加,页面性能依旧会持续走低。而虚拟列表是从DOM渲染层面根治长列表性能问题的最优方案,核心思路是只保留屏幕可视区域内需要展示的DOM节点,彻底销毁可视区外的节点,将页面常驻DOM节点数量恒定控制在极低范围,不受总数据量影响。
虚拟列表的底层逻辑可以概括为可视区裁剪+偏移量占位+数据动态映射三大核心机制,无论列表总数据量是上千条还是上万条,页面中实际存在的DOM节点始终维持在「可视区条数+上下缓冲条数」固定数值,从根源减少DOM节点数量。
虚拟列表需要一个外层滚动容器承接滚动事件,容器内部分为两层结构:第一层是高度等于全部列表总高度的空白占位层,第二层是承载实际列表项的内容渲染层。
空白占位层不会渲染任何业务内容,仅通过固定高度撑起整个滚动区域,模拟完整长列表的滚动高度,保证页面滚动条的长度、滚动范围和原生完整列表完全一致,用户滚动感知无任何差异。总高度计算公式为:列表总条数 × 单条列表项固定高度(定高虚拟列表),若是不定高列表,则需要累计每一条真实渲染项的高度得出总占位高度。
页面存在固定尺寸的可视窗口,结合滚动距离可以实时计算出当前处于可视区内的数据下标范围。滚动容器监听滚动事件后,实时获取垂直滚动偏移量,结合单条列表项高度,计算出:当前滚动起始下标、当前滚动结束下标。
系统只会截取起始下标到结束下标之间的数据进行DOM渲染,其余所有下标对应的数据均不生成DOM节点。为了避免滚动过程中出现空白屏,需要在可视区上下各增加一定数量的缓冲节点,提前渲染即将进入可视区的列表项,保证快速滑动时页面无空白断层。
仅仅裁剪数据还无法实现滚动效果,需要通过CSS transform位移属性,动态改变内容渲染层的垂直偏移量。偏移量等于「当前起始下标 × 单条列表项高度」,让渲染出来的少量列表项,始终精准贴合当前滚动位置,视觉上和完整长列表滚动效果完全一致。
小程序受双线程架构限制(逻辑层、视图层分离,通信存在延迟),不能直接照搬Web端虚拟列表方案,需要适配小程序原生scroll组件能力,分为定高虚拟列表(简单易实现,通用性强)和不定高虚拟列表(适配动态内容列表,复杂度更高)两种实现方式,下文以最常用的定高虚拟列表拆解完整开发流程。
摒弃原生wx:for循环渲染全部列表数据的写法,采用小程序scroll-view作为外层滚动容器,结构分为三层:外层滚动容器、占位高度盒子、实际内容渲染盒子。
外层scroll-view开启垂直滚动,禁止原生滚动回弹避免偏移计算异常;占位盒子绑定动态计算的总列表高度,撑起滚动区域;内容盒子通过transform做垂直位移,内部仅循环渲染当前可视区+缓冲区的少量数据,彻底减少wx:for渲染节点数量。
在页面逻辑层定义固定核心变量,支撑全部计算逻辑:
itemHeight:单条列表项固定高度,提前统一样式固定值;
visibleCount:屏幕可视区域内可展示的列表项数量,由滚动容器高度/单条项高度自动计算;
bufferCount:上下缓冲条数,一般设置4-6条,平衡空白屏和渲染性能;
scrollTop:实时滚动垂直偏移量,由scroll-view滚动事件获取;
startIndex、endIndex:当前渲染数据的起始、结束下标;
renderList:实际页面渲染的切片数据,永远为固定条数。
绑定scroll-view的scroll滚动事件,每次滚动触发时实时获取scrollTop值,同步更新起始下标:startIndex = Math.floor(scrollTop / itemHeight) - bufferCount。为了防止起始下标小于0出现负数,需要做边界兜底,最小起始下标固定为0。
结束下标计算公式:endIndex = startIndex + visibleCount + bufferCount * 2。通过起始和结束下标,从完整源数据中截取对应区间的数据赋值给renderList,页面仅渲染该切片数据。
根据起始下标计算内容容器的垂直偏移距离,偏移值 = startIndex * itemHeight,通过内联样式绑定transform:translateY(${偏移值}rpx),让渲染的列表项跟随滚动位置实时移动,填补上方空白区域,保证视觉滚动连贯。
小程序逻辑层和视图层分离,滚动事件高频触发会造成两层频繁通信,引发延迟和卡顿。需要增加节流函数限制滚动计算频率,将计算频率控制在16ms一次(和浏览器一帧渲染时长对齐),避免主线程被大量计算逻辑阻塞;同时避免在滚动事件中执行setData高频更新数据,合并多次滚动计算结果,减少视图层和逻辑层的数据通信次数。
实际业务中大部分列表项内容不固定,图片、文字长度变化会导致列表项高度动态变化,定高虚拟列表不再适用,需要引入节点高度缓存机制实现不定高虚拟列表。
核心优化逻辑:首次渲染列表项后,通过小程序createSelectorQuery获取每一个已渲染列表项的真实dom高度,存入本地缓存数组;后续滚动计算起始下标、总占位高度时,不再使用固定高度,而是读取缓存内每一条数据的真实高度进行累加计算。
同时需要维护累计高度数组,记录每一个下标对应的累计总高度,通过二分查找算法替代遍历查找,根据滚动距离快速匹配当前起始渲染下标,降低海量数据下的下标查找耗时。不定高方案实现复杂度更高,但可以适配所有动态列表场景,是生产环境更通用的虚拟列表方案。
根源是滚动事件通信延迟,视图层滚动位置更新快于逻辑层数据切片更新。解决方案:合理增大缓冲条数,同时开启scroll-view的被动滚动监听,提升滚动事件响应速度,禁止滚动过程中额外的复杂业务逻辑。
原生下拉刷新和触底加载基于完整列表高度计算,虚拟列表仅有少量DOM节点,会导致触底时机提前或延后。解决方案:监听数据源变化,数据新增或清空后,重新计算总占位高度、重置高度缓存,同步更新滚动容器布局。
图片异步加载会改变列表项真实高度,破坏高度缓存准确性。解决方案:图片标签提前设置固定宽高占位,图片加载完成后重新获取当前节点高度,更新本地高度缓存,同步修正内容容器偏移量。
滚动监听事件、节点查询请求若未销毁,会持续占用页面内存。解决方案:页面卸载时,清除滚动节流定时器、清空高度缓存数组、取消未完成的节点查询请求,释放全部监听资源。
优化方案 |
DOM节点数量 |
内存占用 |
滑动流畅度 |
实现成本 |
|---|---|---|---|---|
原生一次性渲染 |
等于总数据量 |
极高 |
差 |
极低 |
分页懒加载 |
持续累加 |
持续升高 |
一般 |
低 |
虚拟列表 |
固定恒定值 |
极低 |
优秀 |
中高 |
从对比结果可以看出,分页懒加载只能延缓性能恶化,无法根治问题,而虚拟列表从DOM渲染底层解决长列表痛点,无论数据量多大,页面性能始终保持稳定。只有在数据量小于100条的短列表场景下,才无需使用虚拟列表,避免过度优化增加代码复杂度。
小程序虚拟列表的核心本质并不是优化滚动逻辑,而是控制页面常驻DOM节点数量,通过占位层模拟完整列表滚动高度,通过数据切片只渲染可视区域内容,通过CSS位移实现视觉滚动效果,完美规避小程序双线程架构下长列表的渲染、通信、内存三大性能问题。
定高虚拟列表适合样式统一、高度固定的列表,开发成本低、运行稳定;不定高虚拟列表适配全部动态内容场景,依靠高度缓存和二分查找弥补动态高度带来的计算偏差。在实际开发中,只要理清滚动偏移量、数据下标、容器位移三者的联动关系,同时做好小程序双线程通信节流、资源销毁、边界值兜底等适配处理,就能自主实现高性能、无依赖的原生虚拟列表,无需引入第三方组件库,彻底解决小程序长列表所有卡顿、白屏、内存溢出问题。