React Native FlatList 原理解析与性能优化
本文是【React Native 性能优化指南】的一部分内容,因为内容比较具有代表性,所以单独拿出进行讲解;若想获得完整优化建议,可点击原文查看。
在 React Native 开发中,最容易遇到的对性能有一定要求场景就是长列表了。在日常业务实践中,优化做好后,千条数据渲染还是没啥问题的。
虚拟列表前端一直是个经典的话题,核心思想也很简单:只渲染当前展示和即将展示的 View,距离远的 View 用空白 View 展示,从而减少长列表的内存占用。
在 React Native 官网上,🔗 列表配置优化其实说的很好了,我们基本上只要了解清楚几个配置项,然后灵活配置就好。但是问题就出在「了解清楚」这四个字上,本节我会结合图文,给大家讲述清楚这几个配置。
1⃣️ 各种列表间的关系
React Native 有好几个列表组件,先简单介绍一下:
- ScrollView:会把视图里的所有 View 渲染,直接对接 Native 的滚动列表
- VirtualizedList:虚拟列表核心文件,使用 ScrollView,长列表优化配置项主要是控制它
- FlatList:使用 VirtualizedList,实现了一行多列的功能,大部分功能都是 VirtualizedList 提供的
- SectionList:使用 VirtualizedList,底层使用 VirtualizedSectionList,把二维数据转为一维数据
还有一些其他依赖文件,有个🔗 博文的图总结的挺好的,我这里借用它的图一下:
我们可以看出 VirtualizedList 才是主演,下面我们结合一些示例代码,分析它的配置项。
2⃣️ 列表配置项
讲之前先写个小 demo。demo 非常简单,一个基于 FlatList 的奇偶行颜色不同的列表。
export default class App extends React.Component {
renderItem = item => {
return (
<Text
style={{
backgroundColor: item.index % 2 === 0 ? 'green' : 'blue',
}}>
{'第 ' + (item.index + 1) + ' 个'}
</Text>
);
}
render() {
let data = [];
for (let i = 0; i < 1000; i++) {
data.push({key: i});
}
return (
<View style={{flex: 1}}>
<FlatList
data={data}
renderItem={this.renderItem}
initialNumToRender={3} // 首批渲染的元素数量
windowSize={3} // 渲染区域高度
removeClippedSubviews={Platform.OS === 'android'} // 是否裁剪子视图
maxToRenderPerBatch={10} // 增量渲染最大数量
updateCellsBatchingPeriod={50} // 增量渲染时间间隔
debug // 开启 debug 模式
/>
</View>
);
}
}
VirtualizedList 有个 debug 的配置项,开启后会在视图右侧显示虚拟列表的显示情况。
这个属性文档中没有说,是翻🔗 源码发现的,我发现开启它后用来演示讲解还是很方便的,可以很直观的学习 initialNumToRender、windowSize、Viewport,Blank areas 等概念。
下面是开启 debug 后的 demo 截屏:
上面的图还是很清晰的,右侧 debug 指示条的黄色部分表示内存中 Item,各个属性我们再用文字描述一下:
1.initialNumToRender
首批应该渲染的元素数量,刚刚盖住首屏最好。而且从 debug 指示条可以看出,这批元素会一直存在于内存中。
2.Viewport
视口高度,就是用户能看到内容,一般就是设备高度。
3.windowSize
渲染区域高度,一般为 Viewport 的整数倍。这里我设置为 3,从 debug 指示条可以看出,它的高度是 Viewport 的 3 倍,上面扩展 1 个屏幕高度,下面扩展 1 个屏幕高度。在这个区域里的内容都会保存在内存里。
将 windowSize 设置为一个较小值,能有减小内存消耗并提高性能,但是快速滚动列表时,遇到未渲染的内容的几率会增大,会看到占位的白色 View。大家可以把 windowSize 设为 1 测试一下,100% 会看到占位 View。
4.Blank areas
空白 View,VirtualizedList 会把渲染区域外的 Item 替换为一个空白 View,用来减少长列表的内存占用。顶部和底部都可以有。
上图是渲染图,我们可以利用 react-devtools 再看看 React 的 Virtual DOM(为了截屏方便,我把 initialNumToRender 和 windowSize 设为 1),可以看出和上面的示意图是一致的。
5.removeClippedSubviews
这个翻译过来叫「裁剪子视图」的属性,文档描述不是很清晰,大意是设为 true 可以提高渲染速度,但是 iOS 上可能会出现 bug。这个属性 VirtualizedList 没有做任何优化,是直接透传给 ScrollView 的。
在 0.59 版本的一次 🔗 commit 里,FlatList 默认 Android 开启此功能,如果你的版本低于 0.59,可以用以下方式开启:
removeClippedSubviews={Platform.OS === 'android'}
6.maxToRenderPerBatch 和 updateCellsBatchingPeriod
VirtualizedList 的数据不是一下子全部渲染的,而是分批次渲染的。这两个属性就是控制增量渲染的。
这两个属性一般是配合着用的,maxToRenderPerBatch 表示每次增量渲染的最大数量,updateCellsBatchingPeriod 表示每次增量渲染的时间间隔。
我们可以调节这两个参数来平衡渲染速度和响应速度。但是,调参作为一门玄学,很难得出一个统一的「最佳实践」,所以我们在业务中也没有动过这两个属性,直接用的系统默认值。
2⃣️ ListLtems 优化
📄 ListLtems 优化 文档:https://reactnative.cn/docs/optimizing-flatlist-configuration/#list-items
文档中说了好几点优化,其实在前文我都介绍过了,这里再简单提一下:
1.使用 getItemLayout
如果 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就非常的合算。
在源码中(#L1287、#L2046),如果不使用 getItemLayout,那么所有的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是需要消耗时间的;如果我们使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了计算,节省了这部分的开销。
在这里我还想提一下几个注意点,希望大家使用 getItemLayout 要多注意一下:
- 如果 ListItem 高度不固定,使用 getItemLayout 返回固定高度时,因为最终渲染高度和预测高度不一致,会出现页面跳动的问题【🔗 问题链接】
- 如果使用了
ItemSeparatorComponent
,分隔线的尺寸也要考虑到 offset 的计算中【🔗 文档链接】 - 如果 FlatList 使用的时候使用了
ListHeaderComponent
,也要把 Header 的尺寸考虑到 offset 的计算中【🔗 官方示例代码链接】
2.Use simple components & Use light components
使用简单组件,核心就是减少逻辑判断和嵌套,优化方式可以参考「二、减轻渲染压力」的内容。
3.Use shouldComponentUpdate
参考「一、re-render」的内容。
4.Use cached optimized images
参考「三、图片优化那些事」的内容。
5.Use keyExtractor or key
常规优化点了,可以看 React 的文档 🔗 列表 & Key。
6.Avoid anonymous function on renderItem
renderItem 避免使用匿名函数,参考「四、对象创建调用分离」的内容。
最后推荐一下我的个人公众号:「卤蛋实验室」,平时会分享一些前端技术和数据分析的内容,大家感兴趣的话可以关注一波: