React Native 的 FlatList 性能在大多数情况下是很棒的。但是有时候如果数据量比较大时它也存在一些缺陷。我们可以在网络上找到很多 issues 和博客文章讲述如何提高它的性能。在这里,我试图整理一份关于此问题相对想尽的文档。
事先声明,没有所谓的可以解决所有问题的「银弹」。在实际应用场景中,我们仍需思考哪种方式和解决方案最适合我们的业务需求。
一些关系和概念
一开始使用 FlatList 相关组件进行开发的时候,有很多概念让我困惑。所以让我们先将他们来梳理一下:
-
VirtualizedList 是 FlatList 的底层实现,直接使用 FlatList 和 SectionList 会更加方便。一般来说,仅当想获得比 FlatList 更高的灵活性(比如说在使用 immutable data 而不是 普通数组)的时候,你才应该考虑使用 VirtualizedList。具体可以查看官方文档的
Virtualizedlist
部分。也可以参考Virtualized List 这个组件的实现,它提供 React 网页版本的列表实现,支持显示大量数据,针对数据显示的列表内组件自动回收,拥有出色的性能。 - Performance 性能 此文中,我们主要指列表性能,即为用户提供顺滑的滚动浏览内容的体验。
- Memory consumption 内存消耗 列表数据在内存中占用多大,这关系到 APP 的稳定性,如果占用内存过大,可能导致你的应用崩溃(被操作系统终止)。
- Responsiveness 响应能力 这里指应用对交互的响应能力的快慢。举个例子,如果用户点击列表中的某一项之后要弹出提示或者跳转页面,如果跳转页面很慢的话,我们称之为响应能力比较弱。
- Blank areas 空白区域 如果列表来不及渲染具体的列表项(如快速滚动列表的过程中),你将看到列表的背景,也就是一片白色。
- Window 视窗 这里指列表内容的可视化区域。
Props(组件属性)
调整和优化组件属性是一种可行的优化 FlatList 性能的方式。以下是几种关于这种思路的调整方式。
设定 removeClippedSubviews 属性
你可以设定 removeClippedSubviews 属性为 true
(默认为 false),如果列表项组件滚动到列表的可视区域之外会自动卸载(unmount)。
优点: 这种方式对内存占用比较友好,FlatList 将仅渲染一部分列表项,而非渲染所有数据,类似 iOS 中的 TableView。
缺点:如果遇到频繁、高速的滚动,会导致大量的列表项组件初始化和卸载动作,虽然内存占用被减少,但随之而来的是计算量的大量增加,对于一些性能不够高的设备来说会出现明显的卡顿现象,影响用户体验。如果列表项组件内包含一些复杂的初始化操作和数据引用,可能会导致一些问题或内存泄漏。根据官方文档标注,开启此设定后在有些情况下会有 bug(比如内容无法显示),需谨慎使用。(尝试设定 removeClippedSubviews 为 true 后,测试一个 50 项列表的渲染,频繁滚动它,可以看到 CPU 占用有 5-10% 的上浮)
maxToRenderPerBatch
可参考官方的 VisualizedList
API 说明,摘要如下:每批增量渲染可渲染的最大数量。能立即渲染出的元素数量越多,填充速率就越快,但是响应性可能会有一些损失,因为每个被渲染的元素都可能参与或干扰对按钮点击事件或其他事件的响应。这个属性默认数值是 10。
优点: 如果我们设定一个比较大的数值,每批渲染的列表项比较多,这样在滚动的时候会更小概率出现空白区域(未及时渲染)显示。
缺点:参考文档我们可以了解到新的一批列表项在滚动到可视区域前就开始异步渲染,每批数量越大,所需要的计算量越大。这可能会阻塞 JS 线程,导致应用程序无法快速响应用户的点击事件。如果实际应用场景中需要渲染的列表是静态的、不需要响应用户交互的,将此属性数值调大是一个不错的选择。
updateCellsBatchingPeriod
用于具有较低渲染优先级的元素(比如那些离屏幕相当远的元素)的渲染批次之间的时间间隔。与 maxToRenderPerBatch 具有相同的目的,都是为了在渲染速率和响应性之间获得一个平衡。
这个属性的单位是毫秒,默认值是 50 毫秒。
优点: 可以与 maxToRenderPerBatch
属性配合来调整渲染的节奏,找到更适合你应用场景的方式。
缺点:性能和体验的平衡是很难平衡的,性能和视觉体验的抉择是有点困难的。
initialNumToRender
设定列表组件初始化渲染时默认渲染的列表项数量,默认为 10。如果每一个列表项的高度比较大,也许数值设定的更小会更加合适。当然,需要填充可视区域,否则用户将看到空白区域。
windowSize
设置可视区外最大能被渲染的元素的数量,以可视区的长度为单位。比如说,如果列表占满了整个屏幕,而 windowSize 属性被设置为 21 的话,那渲染的长度为包括当前可见屏幕区域在内,往上 10 个屏幕的长度和往下 10 个屏幕的长度。将 windowSize 设置为一个较小值,能有减小内存消耗并提高性能,但是当你快速滚动列表时,遇到尚未渲染的内容的几率会增大,而这些尚未渲染的内容会暂时性地被空白区块所替代。
优点: 在性能允许的情况下,可以设定一个大的数值来保证更加流畅的用户体验,并避免空白区域显示,比如应用程序仅需要支持几款比较新的特定设备。如果考虑兼容更多的设备(比如已经发布了很久的机型),建议尽量将此数值调低一些。
缺点:如果将数值调大,将会导致更多的内存占用,这对一部分设备来说会比较吃力。如果数值设置的太小,会导致更高频的计算。
legacyImplementation
如果此属性设定为 true
,将基于 ListView
进行实现。默认值为 false
。
优点: 一次性渲染列表所有项,没有 VisualizationList
组件实现方式带来额外计算开销,用户滚动列表将变得非常流畅。
缺点:如果列表内容很多,将占用很多内存空间,触发内存警告,甚至会导致应用内存占用过大被系统杀死。
disableVirtualization
这个属性已经被取消了,我们这里就不讲了。
List items
除了优化列表属性,也有一些列表项渲染的优化策略。
让列表项组件逻辑尽可能简单
列表项组件越复杂,渲染越慢。我们需要尽可能的避免在列表项组件中编写复杂的业务逻辑,如果你的列表项组件在应用多处复用,建议尽量为数据量较大的列表创建一个单独的、简化逻辑的版本来单独使用。
让组件尽可能变得轻量
避免在列表项组件中加载过多的图片等资源。建议与设计团队沟通,尽可能的减少列表项的交互,通过跳转或其他方式来实现更多交互。
使用 shouldComponentUpdate 生命周期
为列表项组件增加更新判断逻辑。React 的 PureComponent 默认通过数据的浅比较来实现 shouldComponentupdate
。由于这种实现方式需要检查你的每一个属性,代价是比较高的。如果我们需要实现更好的性能,我们需要为列表项组件创建严格的属性规则,仅检查可能出现变动的特定属性。如果你的组件足够简单,甚至可以这样做:
shouldComponentUpdate() {
return false
}
使用经过优化的图片组件
我个人习惯使用 @DylanVann 的 react-native-fast-image 组件。列表项组件中的每一个图片都是一个 new Image()
实例。图片实力越早完成 loaded
状态,JavaScript 线程将越早被释放资源。
使用 getItemLayout
getItemLayout 是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,getItemLayout 用起来就既高效又简单。
getItemLayout = (data, index) => ({
length: 70,
offset: 70 * index,
index
})
使用 keyExtractor
此函数用于为给定的 item 生成一个不重复的 key。Key 的作用是使 React 能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取 item.key 作为 key 值。若 item.key 也不存在,则使用数组下标。
keyExtractor={item => item.id}