一、背景
1.1 简介
常规的长列表解决方案,包括无限加载、分页加载或虚拟列表等,但在一些特殊场景下无法用单一的技术解决问题,比如在移动端 H5 页面中在页面最底部有一个无限加载的 feeds 列表,此时使用无限加载、分页加载会有明显的响应度问题,使用虚拟列表会有白屏、滚动嵌套的问题,因此一个无限滚动的 feeds 列表并没有想象的那么简单。
1.2 场景举例
假设有一个移动端的 H5 页面:提供帖子列表功能,其表现形式为 Feeds 流,封装一个【无尽滚动列表】从交互的角度提升用户体验,提升页面的响应度。
其出现位置如下:
1.3 开发挑战
在业务复杂度、交互复杂度、性能体验稳定性要求都很高的今天,一个大的长列表无疑给技术开发带来巨大的挑战,用户滑动页面不断加载 feeds 数据, DOM 节点不断累加创建,因为 DOM 节点过多,导致页面数据更新时卡顿问题。
解决以上问题一般会通过虚拟滚动减少页面 DOM 数量,但虚拟列表自身存在一定问题并非适合所有场景,其存在以下特点:
- 用计算机算力和浏览器的渲染能力换取 DOM 数的减少,其最直接的影响是在用户在机器配置较低或者 js 线程较忙时触发滚动,尤其是快速滚动的情况下,因来不及渲染出现白屏
- 因滚动容器高度需要参与计算,因此其实现方式往往需要固定容器高度,在首页有前置内容的前提下实际上无法提供固定高度,即便提供了具体高度也会发生滚动嵌套等情况
综上所述,虚拟列表比较适合列表项较多、列表项内容简单和滚动容器高度固定的场景,显然在示例这个场景直接使用虚拟列表并不合适。
为了系统化的解决这个问题,一方面需要对已有的技术进行分析总结,另外一方面需要基于总结并结合实际给出体系化的解决思路并落地。
二、目标和规划
2.1 目标
基于用户感知实现,目标是让用户花在网站上的大多数时间不是等待加载,而是在使用时等待响应,其中的"响应度"不仅仅是实现层面问题,更是个设计问题,仅靠性能优化与更快的硬件是难以解决的。
简而言之,就是不强制打断用户的操作行为,让用户在操作动作进行的同时等待加载
研究人员在过去 50 年里一致发现,一个交互系统的响应度,即能否跟上用户、及时告知当前状态,而不让他们无故等待,是决定用户满意度的最重要的因素。
From: Designing with the Mind in Mind - Jeff Johnson
2.1.1 行业标准
大脑需要多少时间去"感觉"与"认知"?
人们对于延迟和停顿的感知能力也有一定的反应时间。
RAIL 性能模型
Google RAIL 性能模型的目标,就是使用户满意,而不是让页面能运行的很快。
其关键指标内容如下:
2.1.3 短期目标
如果需求属于探索型 需求,其形态可能会随着后期效果和数据观测进行调整。这种情况下对开发工作的影响,要求代码尽量追求张弛有度,求稳求速,无须追求极致完美。
因此 Feeds 列表一期的目标可以为:
- 采用分页加载技术为主,以最稳定、稳妥的形式展现列表,降低复杂技术的开发风险
- 辅以预加载 + 占位图技术,提升列表响应度,从而提升用户感知,控制用户滚动行为停顿时间小于 100ms
通过预加载提前获取数据,比如首页有50条数据,用户划到30条的时候就开始加载下一页的数据。 通过占位图,实现无阻赛的滚动,即:用户滚动到当前页数据结尾时,或者在预加载开始时,就直接填充下一页列表,只不过是空数据,展现效果为占位符,等接口返回数据时再替换占位符。
2.1.3 长期目标
依实际情况进行调整
以分页加载 + 虚拟列表 + 占位图为主的技术形式实现,辅以定制化虚拟列表,从而解决用户滑动页面不断加载 feeds 数据, DOM 节点不断累加创建,因为 DOM 节点过多,导致页面数据更新时卡顿问题。
三、实现细节
3.1 分页加载
列表滚动时"停顿感"的产生
-
用户滑动
-
等待数据加载
-
渲染新数据
-
用户继续滑动,查看新渲染的内容
"滚动事件触发 -> 异步获取数据 -> 渲染" 这样的机制,页面响应用户操作,并渲染新内容可能需要花费大于500ms的时间。
尽管提供了spinner等友好的反馈提示,但用户切实的感受到了"等待加载"这一过程,而且需要再次滚动,才能浏览新渲染的内容。
3.2 预加载
预加载是常用的优化手段之一,设置一定的阈值,在用户滚动到页面底部前获取数据,加载下一页的数据并渲染。
预加载能有效的提升体验,但是在弱网环境或用户快速滚动时,底部的loading依然会经常性的触发。
而一味的提高提前加载的阈值,肯定不是个好办法,首先是网络状况等情况随时可能改变,导致边界难以确定,其次是必定会产生大量冗余的数据请求。
3.3 无阻塞的滚动
这是一种比较理想的列表运行方式,滚动时永远不打断用户操作,数据不足时根据滚动位置按需加载,先插入占位元素,等到数据获取时再渲染新的元素。
3.4 虚拟列表
尽管这种懒加载的分页加载可以解决网络请求和首屏加载的问题,但随着数据增加,DOM 元素的数量也会不断增加,可能导致页面出现卡顿的情况。
为了解决这个问题,我们可以引入虚拟列表的概念和实现方法。虚拟列表的原理和实现思路已经在网上有很多资料,这里就不再赘述。
虚拟列表的主要目标是解决列表渲染性能问题,并解决随着数据增加而导致的 DOM 元素过多的问题。
3.5 分页加载 + 虚拟列表 + 占位图
- 列表组件只关心和渲染限定范围(视窗可见区域+前后预Buffer数量)内的元素。
- 每当滚动触发导致元素移出视窗,组件会计算并填充渲染范围内变化的部分,并回收多余的DOM节点。
- 如果渲染范围内的元素还未取到所需的数据,则会先渲染占位元素,同时将缺失的数据条数通知给调用方,在异步数据返回后渲染并替换占位元素。
3.6 定制化虚拟列表
探索定制化虚拟列表(分页加载 + 虚拟列表 + 占位图)实现的可能性,实现形式如下:
- 固定列表高度,动态开启容器滚动实现
- 整屏虚拟列表,首页全部内容都在虚拟列表中