RN版本:0.64
系统:Win10
前言
尽管facebook已经尽可能地优化 React Native 性能来了,但是,总还是有一些地方有所欠缺,以及在某些场合 React Native 还不能够替我们决定如何进行优化,因此人工的干预依然是必要的。
1.减少页面内重绘制
在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。如要避免不必要的子组件的重渲染,有以下途径:
- 实现shouldComponentUpdate函数来指明在什么样的确切条件下,希望组件得到重绘
// 示例
class Button extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
return false;
}
render() {
return ;
}
}
- React.memo 和 useMemo
// React.memo
// 示例
// 默认比较函数
const MemoButton = React.memo(function Button(props) {
return ;
});
// 自定义比较函数
function Button(props) {
return ;
}
function areEqual(prevProps, nextProps) {
if (prevProps.color !== nextProps.color) {
return false;
}
return true;
}
export default React.memo(MyComponent, areEqual);
//解决因函数更新而渲染自己的问题,就可以使用useMemo,使用它将函数重新封装
const onClick=useMemo(()=>{
return ()=>{
console.log(m)
}
},[m])
//等价于
const onClick=useCallback(()=>{
console.log(m)
},[m])
- 如果编写的是纯粹的组件(界面完全由 props 和 state 所决定),可以使用PureComponent来获得更好的性能
// 示例
class PureComponentButton extends React.PureComponent {
render() {
return ;
}
}
2.减轻渲染压力
- 使用 React.Fragment 避免多层嵌套
// 示例
render() {
return (
);
}
// 或者使用 Fragment 短语法
render() {
return (
<>
>
);
}
- 减少 GPU 过度绘制
- 减少背景色的重复设置:每个 View 都设置背景色的话,在 Android上会造成非常严重的过度绘制;并且只有布局属性时,React Native 还会减少 Android 的布局嵌套
- 避免设置半透明颜色:半透明色区域 iOS Android 都会引起过度绘制
- 避免设置圆角:圆角部位 iOS Android 都会引起过度绘制
- 避免设置阴影:阴影区域 iOS Android 都会引起过度绘制
3.对象创建调用分离
在 JS 引擎里,创建一个对象的时间差不多是调用一个已存在对象的 10 多倍。在绝大部分情况下,这点儿性能消耗和时间消耗不值一提。但在这里还是要总结一下,因为这个思维习惯还是很重要的。
1. public class fields 语法绑定回调函数项
在 React 上如何处理事件已经是个非常经典的话题了,最常见的绑定方式应该是直接通过箭头函数处理事件:
class Button extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
return ;
}
}
但这种语法的问题是每次 Button 组件重新渲染时,都会创建一个 handleClick() 函数,当重绘制的次数比较多时,会对 JS 引擎造成一定的垃圾回收压力,会引起一定的性能问题。
官方文档里比较推荐开发者使用 public class fields 语法 来处理回调函数,这样的话一个函数只会创建一次,组件 重绘制时不会再次创建:
class Button extends React.Component {
// 此语法确保 handleClick 内的 this 已被绑定。
handleClick = () => {
console.log('this is:', this);
}
render() {
return ;
}
}
在实际开发中,经过一些数据对比,因绑定事件方式的不同引起的性能消耗基本上是可以忽略不计的,重绘制 次数过多才是性能杀手。
2. public class fields 语法绑定渲染函数
这个其实和第一个差不多,只不过把事件回调函数改成渲染函数,在 React Native 的 Flatlist 中很常见。
很多人使用 Flatlist 时,会直接向 renderItem 传入匿名函数,这样每次调用 render 函数时都会创建新的匿名函数:
render(){
{item.title} }
/>
}
改成 public class fields 式的函数时,就可以避免这个现象了:
renderItem = ({ item }) => {item.title} ;
render(){
}
同样的道理,ListHeaderComponent 和 ListFooterComponent 也应该用这样写法,预先传入已经渲染好的 Element,避免 re-render 时重新生成渲染函数,造成组件内部图片重新加载出现的闪烁现象。
3. StyleSheet.create 替代 StyleSheet.flatten
StyleSheet.create 这个函数,会把传入的 Object 转为优化后的 StyleID,在内存占用和 Bridge 通信上会有些优化。
const styles = StyleSheet.create({
item: {
color: 'white',
},
});
console.log(styles.item) // 打印出的是一个整数 ID
在业务开发时,我们经常会抽出一些公用 UI 组件,然后传入不同的参数,让 UI 组件展示不一样的样式。
为了 UI 样式的灵活性,我们一般会使用 StyleSheet.flatten,把通过 props 传入自定义样式和默认样式合并为一个样式对象:
const styles = StyleSheet.create({
item: {
color: 'white',
},
});
StyleSheet.flatten([styles.item, props.style]) // <= 合并默认样式和自定义样式
这样做的好处就是可以灵活的控制样式,问题就是使用这个方法时,会 递归遍历已经转换为 StyleID 的样式对象,然后生成一个新的样式对象。这样就会破坏 StyleSheet.create 之前的优化,可能会引起一定的性能负担。
当然本节不是说不能用 StyleSheet.flatten,通用性和高性能不能同时兼得,根据不同的业务场景采取不同的方案才是正解。
4. 避免在 render 函数里创建新数组/对象
我们写代码时,为了避免传入 [] 的地方因数据没拿到传入 undefined,经常会默认传入一个空数组:
render() {
return
}
其实更好的做法是下面这样的:
const EMPTY_ARRAY = [];
render() {
return
}
4.动画性能优化
1. 开启 useNativeDrive: true
JS Thread 和 UI Thread 之间是通过 JSON 字符串传递消息的。对于一些可预测的动画,比如说点击一个点赞按钮,就跳出一个点赞动画,这种行为完全可以预测的动画,我们可以使用 useNativeDrive: true 开启原生动画驱动。
通过启用原生驱动,我们在启动动画前就把其所有配置信息都发送到原生端,利用原生代码在 UI 线程执行动画,而不用每一帧都在两端间来回沟通。如此一来,动画一开始就完全脱离了 JS 线程,因此此时即便 JS 线程被卡住,也不会影响到动画了。
使用也很简单,只要在动画开始前在动画配置中加入 useNativeDrive: true 就可以了:
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // <-- 加上这一行
}).start();
开启后所有的动画都会在 Native 线程运行,动画就会变的非常顺畅。
值得注意的是,useNativeDriver 这个属性也有着局限性,只能使用到只有非布局相关的动画属性上,例如 transform 和 opacity。布局相关的属性,比如说 height 和 position 相关的属性,开启后会报错。而且前面也说了,useNativeDriver 只能用在可预测的动画上,比如说跟随手势这种动画,useNativeDriver 就用不了的。
5.长列表性能优化
在 React Native 开发中,最容易遇到的对性能有一定要求场景就是长列表了。在日常业务实践中,优化做好后,千条数据渲染还是没啥问题的。
虚拟列表前端一直是个经典的话题,核心思想也很简单:只渲染当前展示和即将展示的 View,距离远的 View 用空白 View 展示,从而减少长列表的内存占用。(这一点和Android的RecycleView比较类型)
在 React Native 官网上, 列表配置优化其实说的很好了,我们基本上只要了解清楚几个配置项,然后灵活配置就好。但是问题就出在「了解清楚」这四个字上,本节我会结合图文,给大家讲述清楚这几个配置。
1.各种列表间的关系
React Native 有好几个列表组件,先简单介绍一下:
ScrollView:会把视图里的所有 View 渲染,直接对接 Native 的滚动列表
VirtualizedList:虚拟列表核心文件,使用 ScrollView,长列表优化配置项主要是控制它
FlatList:使用 VirtualizedList,实现了一行多列的功能,大部分功能都是 VirtualizedList 提供的
SectionList:使用 VirtualizedList,底层使用 VirtualizedSectionList,把二维数据转为一维数据
2.列表配置项
一个基于 FlatList 的奇偶行颜色不同的列表
// 一个基于 FlatList 的奇偶行颜色不同的列表
export default class App extends React.Component {
renderItem = item => {
return (
{'第 ' + (item.index + 1) + ' 个'}
);
}
render() {
let data = [];
for (let i = 0; i < 1000; i++) {
data.push({key: i});
}
return (
);
}
}
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 表示每次增量渲染的时间间隔。
3.ListLtems 优化
1.使用 getItemLayout
如果 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就非常的合算。
如果不使用 getItemLayout,那么所有的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是需要消耗时间的;如果我们使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了计算,节省了这部分的开销。
如果 ListItem 高度不固定,使用 getItemLayout 返回固定高度时,因为最终渲染高度和预测高度不一致,会出现页面跳动的问题
如果使用了 ItemSeparatorComponent,分隔线的尺寸也要考虑到 offset 的计算中
如果 FlatList 使用的时候使用了 ListHeaderComponent,也要把 Header 的尺寸考虑到 offset 的计算中
2.Use simple components & Use light components
使用简单组件,核心就是减少逻辑判断和嵌套
3.Use shouldComponentUpdate
4.Use cached optimized images
5.Use keyExtractor or key
常规优化点了,可以看 React 的文档 列表 & Key。
6.Avoid anonymous function on renderItem
renderItem 避免使用匿名函数