本文将简单介绍一下我所收集到的React Native应用优化方法,希望对你有所启发。很多方法也是适用React web应用的。
包体积优化
无论是热更新方案走网络下载js,还是直接将js打进apk,减小js bundle体积都很必要。
走网络的js体积大影响首次加载速度,打进apk的增加包体积。
- 压缩
为了测试,直接使用react-native init
命令生成了一个rn工程,将其中的App.js改为下面这样简单的代码,验证这样简单的代码打包生成的js bundle体积情况。
import React from 'react';
import {
Text,
} from 'react-native';
const App: () => React$Node = () => {
return (
1
)
};
export default App;
使用 下边的命令可以打包js bundle
# 非压缩
react-native bundle --entry-file ./index.js --bundle-output ./testBundle.js --dev true
# 压缩
react-native bundle --entry-file ./index.js --bundle-output ./testBundle_min.js --dev false
# 查看体积
ls -h testBundle.js testBundle_min.js
-rw-r--r-- 1 bingxiongyi staff 3.3M Dec 24 11:02 testBundle.js
-rw-r--r-- 1 bingxiongyi staff 629K Dec 24 11:06 testBundle_min.js
可见不压缩的体积远远大于压缩后的体积,因此压缩十分必要。将js打包进apk的,默认就会压缩。
关键要注意的是走网络下载js的这种方式需要压缩js.
- 包拆分
上边可以看到一个空工程打包出来的js就压缩后有629k, 他们主要是react-native,react这些框架的代码。
如果是纯的react-native应用,我们可能会直接用react-navigation做路由,这个应用就是生成一个js bundle,打到apk, 框架部分的代码不会重复生成,自然没有什么问题。
但是大多情况为了实现热更新,react-native是嵌入到原生app中的,可能一个页面就会是一个js bundle, 走网络下载,如果每个js bundle都包含框架的代码,必然造成不必要的重复下载。
因此,需要将框架和不常变动的代码单独抽取,最好是将这部分代码打进apk, 这样业务代码体积会大大减小。实际发现我们一个包含三个复杂页面的js bundle业务代码压缩体积仅仅180k多。
关于如何拆包可参考React native 拆包
render次数优化
为什么要减少render次数
走到了render并不一定会有真实dom的操作,但是一定会走一次dom diff。react大名鼎鼎的diff算法确实高效,看了一眼vitual-dom原理与简单实现,确实像大佬们说的,是个O(n)的算法。
需要更新dom时去算diff无可厚非,但是diff了半天发现无差别,不需要更新就白瞎了,O(n)虽好,做不必要的O(n)也是一件浪费资源的事,因此需要尽可能减少diff次数,也就是减少render次数。
如何减少render的次数
- 减少setState的次数
需要说明的是setState可以是批量的,可能多次setState只会有一次render; 也可以是setState一次就render一次。
- 批量:在合成事件, 生命周期函数中的setState是异步批量更新的, 多次setState只会走一次render
- 非批量:在setTimeOut, setInterval, 原生事件, Promise中的setState是同步逐个更新的, 而且每次setState都会走一次render
因此减少setState次数需要减少的是非批量的情形,在合成事件,比如onClick里边去多次setState是没有关系的。
举个例子:
通常写一个上拉加载的列表会像下边这样,每次请求开始时将状态置为加载中。但是可能进页面其实就是加载中的状态了。
getList(pageNum) {
this.setState({
status: 1, // 1 loading, 2 success, 3 error
});
fetchList(pageNum)
.then(res => {
this.setState({
data: [...this.statedata, ...res],
});
})
.catch(e => {
console.log(e);
});
}
这样存在的一个问题是第一页会多一次不必要的setState, 因为初始状态一般就是加载中,当你使用Component而且没有重写shouldComponentUpdate时,这会导致一次不必要的render的。改成下边这样就ok了。
getList(pageNum) {
// 加上这样一个判断
if (this.state.status !== 1) {
this.setState({
status: 1,
});
}
fetchList(pageNum)
.then(res => {
this.setState({
data: [...this.state.data, ...res],
});
})
.catch(e => {
console.log(e);
});
}
第一段代码首屏会有3次render, 第二段代码只有两次。
- 使用PureComponent
Component是每次setState都会去render, 即使你setState的值和之前相同,也会render。
PureComponent重写了shouldComponentUpdate,在里边做了一次浅比较,如果setState后新state和旧state相同是不会走render的。
潜在的问题是当你把state里一个对象的某个属性值改了,由于浅比较是相等的,所以不会走render, 造成显示异常,这个注意下就行。
举个例子
还是上边这个,使用会render 3次的方案,但是使用PureComponent。
class App extends React.PureComponent{}
可以发现也只render了两次。这是因为我们的那次多余的setState set的是和原来相同的值,浅比较相等,所以没有render。
- 重写shouldComponentUpdate
这里有一个react生命周期的图, 从图中可以看出更新的时候每次render之前都会走shouldComponentUpdate, 当shouldComponentUpdate返回false的时候就不会render了,因此我们可以在shouldComponentUpdate中合理控制是否render.
同上,使用会render 3次的方案。
重写一下shouldComponentUpdate, 也来做一个浅比较。
shouldComponentUpdate(nextProps, nextState) {
for (let key in nextState) {
if (nextState[key] !== this.state[key]) {
return true;
}
}
for (let key in nextProps) {
if (nextProps[key] !== this.props[key]) {
return true;
}
}
return false;
}
发现这样做后也只render了2次。
- 减少
diff代价(这个不是减少次数的)
将state的尽量放在更小的组件中,这样render时计算diff的成本更小一些.
举个例子
import React from 'react';
import { Text, Button, View } from 'react-native';
class SubComponent extends React.Component {
constructor(props, context) {
super(props, context);
}
state = {
text: 'A',
};
onPress = () => {
this.setState({
text: 'B',
});
};
render() {
console.log('SubComponent render');
return ;
}
}
class SubComponent2 extends React.Component {
render() {
console.log('SubComponent2 render');
return (
F
)
}
}
function FunctionComponent() {
console.log('FunctionComponent excuted')
return G
}
class App extends React.Component {
constructor(props, context) {
super(props, context);
}
state = {
text: 'D',
}
onPress = () => {
this.setState({
text: 'E'
})
}
render() {
console.log('App render');
return (
)
}
}
export default App;
点红色按钮执行结果如下
App render
App.js:20 SubComponent render
App.js:27 SubComponent2 render
App.js:36 FunctionComponent excuted
点蓝色按钮执行结果如下
SubComponent render
若父组件内状态变更,则他和他所有子组件都会render, 而子组件的变更则不会影响到父组件和兄弟组件。因此在实际开发中应该尽量减小state的作用范围,如果一个状态能收敛到组件内部,就不应该放在外边。这样可以降低render操作的代价。
动画优化
- 启用原生动画渲染
Animated的 API 是可序列化的(即可转化为字符串表达以便通信或存储)。通过启用原生驱动,我们在启动动画前就把其所有配置信息都发送到原生端,利用原生代码在 UI 线程执行动画,而不用每一帧都在两端间来回沟通。如此一来,动画一开始就完全脱离了 JS 线程,因此此时即便 JS 线程被卡住,也不会影响到动画了。(来自reactnative中文文档)
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // <-- 加上这一行
}).start();
- setNativeProps
setNativeProps方法可以使我们直接修改基于原生视图的组件的属性,而不需要使用setState来重新渲染整个组件树。
如果我们要更新的组件有一个非常深的内嵌结构,并且没有使用shouldComponentUpdate来优化,那么使用setNativeProps就将大有裨益。
(来自reactnative中文文档)
- InteractionManager
Interactionmanager 可以将一些耗时较长的工作安排到所有互动或动画完成之后再进行。这样可以保证 JavaScript 动画的流畅运行。
如果动画执行期间可能有比较耗时的代码可以如下操作
InteractionManager.runAfterInteractions(() => {
// 把耗时比较多的代码放到这里,防止他们影响动画效果
});
过渡绘制优化
过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。
在rn应用开发中解决过度绘制问题方法如下:
子组件能将父组件占满的情况下不要父组件背景色,而是指定子组件背景色
这里可以看个例子来了解为什么会这样
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
bg: {
backgroundColor: '#aaa',
},
text: {
padding: 30,
}
})
export default class App extends React.PureComponent {
render() {
return (
0次
1次
2次
3次
4次
);
}
}
这个例子中我们不断的叠加颜色,这样就会产生过度绘制问题,我们可以在开发者选项开启"显示过度绘制"查看效果,如下
从这个例子可以看出我们应该尽量减少不必要的背景叠加来减少过度绘制。
小结
本文的优化方法基本都是从前辈大佬文章借鉴来的,部分方法做了一些小测试,大多没有。有一些方法也是适用react web应用的。
写的比较粗,后边可能会对这些优化点逐一测试,做一些数据上的支撑,实实在在看看这些方法对性能有何种提升。使用的工具应该会是腾讯的性能狗, 非常容易用,大家感兴趣也可以先测测。
参考文章
- React Native 性能优化总结(持续更新。。。)
- react native 中文网
- 你真的了解过度绘制吗?
- React native 拆包