rn性能优化 结合网上资料总结如下
1、首屏渲染问题。采用JS Bundle拆包解决。就是主体框架react单独打成一个基础包,一旦进入app就马上加载,而相关业务模块单独拆分成多个包,进入相应模块才动态加载。这样可以大大加快APP的启动速度,各个业务也能独立开发,各自维护、下载、更新
2、图片问题。rn开发时本地图标为了统一往往放在js端,极端时(如一个页面加载几十上百张图片)可能会有性能问题。这是因为如果资源从 javascript 包中加载, RN 需要先从包中拿到资源,然后通过bridge把资源传送到 原生UI 层去渲染。而如果资源已经存在在原生端,那么 React 可以直接告知 UI 层去渲染具体的图片,无需通过这个bridge引入或者转入图片资源。 当然不会有这类问题,但是要js端图片要注意压缩,使其不太大,图片越大,性能问题越容易凸显。webP,jpg优先
3、缓存。各种需要的,有必要的缓存,如一个生日日期选择picker组件,数据源大概有100(年)x12(月)x30(天)这么多条数据,如果每次弹出picker都需要计算这些数据,还是会稍微有点延迟,这里可以缓存下来,甚至本地数据存储起来,以后拿出来直接使用
4、延迟加载。页面打开,优先执行那些跟页面展示有关的代码,其他的如埋点,上传状态,gif动画都可以稍后执行。对那些触摸响应事件后才需要展示的组件,或者根据接口返回才能决定是否展示的组件,一开始甚至都可以不用import,直到确定要展示时才局部import导入组件展示。对长列表页面,图片较多时,在页面范围之外的图片可以先不展示,直到滚动后发现图片在屏幕上面显示了再展示
5、动画。普通动画如移动,缩放等直接使用LayoutAnimation,性能更好。复杂点的动画才使用Animated。对帧动画这种需要快速更新state触发动画的场景,可以使用setNativeProps直接修改原生属性(某些场合如背景动画,gif图片可能不是很好的选择,因为gif可能会很大,导致初次解压时出现明显卡顿现象,而且安卓上gif图片首轮显示效果不佳)。
Animated: useNativeDriver为true,则会一次性将动画信息发送给原生端让原生去驱动动画,性能更佳。 否则js端会不断注册定时器事件,让原生端不断回调js方法更改组件的setNativeProps值产生动画,因为动画配置信息在每一帧都在原生和js端通信性能有所损耗,
问题: 为什么不总是使用useNativeDriver? 是因为有些动画原生不支持么?
6、响应速度。由于js是单线程,当在执行一些计算量很大的任务时可能会造成堵塞卡顿现象。此时可以将任务稍微延后执行,避免大量任务在同一个js 事件循环中导致其他任务无法执行。相应的方法有InteractionManager,requestAnimationFrame,setTimeOut(0)等,原理都大同小异
7、刷新问题。每次setState导致的render都会进行一次内存中diff计算,尽管diff效率很高(O(n)),但是还是应该避免不必要的diff。 Pure组件、自定义shouldComponentUpdate实现避免不必要的刷新
8、预加载。对一些重要的,很可能会用到的内容预先加载,例如图片浏览器,当浏览某一张图片时可以预加载前后两张图片,优化用户体验。
9、FlatList的优化。
页面中的重头戏FlatList,尽管经过了大量优化,在数据较多时使用还是需要注意的。
FlatList的频繁刷新问题很常见,如下面
class FlatListTest extends React.Component {
state = {
index: 1,
data: []
}
componentDidMount() {
let data = [];
for (let index = 0; index < 100; index++) {
data.push(index);
}
this.setState({ data })
}
renderItem = (item) => {
console.log('表格刷新了');
return (
{item.item}
)
}
render() {
console.log('页面刷新了');
return (
index + ''}
renderItem={this.renderItem}
ListFooterComponent={ }
/>
this.setState({ index: this.state.index + 1 })}>
)
}
}
这样子写FlatList看起来没什么问题,但是性能上完全具有优化空间
点击下方红色按钮让index累加,页面会刷新,但是也会导致FlatList刷新,renderItem被调用98次,就是说页面刷新->表格刷新->所有的表格cell也会刷新。很显然当有大量cell时容易造成性能问题。
FlatList是一个PureComponment,只会对传入的属性进行浅比较(对象地址比较),发现不一样就会刷新。
例子中,FlatList的style,keyExtractor,ListFooterComponent这三个地方传入的对象在页面刷新时会重新生成,导致传入FlatList的属性地址发生变化,FlatList刷新。可以采用下面的方式修复。
renderFooter = () => {
return
}
keyExtractor = (_, index) => {
return index + ''
}
getItemLayout = (_, index) => {
return { length: 50, offset: 50 * index, index }
}
render() {
console.log('页面刷新了');
return (
this.setState({ index: this.state.index + 1 })}>
)
}
const styles = StyleSheet.create({
flatStyle: { width: SCREEN_W, height: 444 }
});
原则就是确保页面刷新后,传入FlatList的所有对象地址不发生变化,这样就不会导致不必要的刷新。
getItemLayout的设置也比较重要,设置后,则列表滚动时,新出现cell时就不用动态去测量cell高度,可以直接从这里拿到,优化性能
把上面的实现改成Hook,会发现页面刷新又会导致表格刷新,因为Hook组件每次刷新时内部的函数都会被重新定义,也就是函数地址发生了变化,从而导致FlatList的刷新。这里需要使用useCallback将所有函数都缓存好,避免函数组件刷新导致函数从新被定义,如下这样,注意依赖
const renderItem = useCallback((item) => {
console.log('表格刷新了');
return (
{item.item}
)
}, [])
本人项目中有个类似微信朋友圈的列表,当数据很多时,在debug环境下点击图片浏览时稍微会有卡顿现象。纠其原因就是因为点击图片浏览触发页面刷新,所有cell跟着刷新完成后才会显示大图导致卡顿,用如上优化后就ok了。
其他:
FlatList显示规则是,在ScrollView上面添加View,只渲染当前展示和即将展示的 View,距离远的 View 用空白 View 展示,从而减少长列表的内存占用。
FlatList的item无法复用,目前了解到的是跟js单线程有关,具体不太明白
重要属性:
getItemLayout,如果不使用,那么所有的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是需要消耗时间的;
为什么需要动态计算每一个View高度? 想一想如果不测量,那么原生端View的Frame如何设置就可以理解了。
windowSize: 表征缓存屏幕外的item多少,单位是一个屏幕显示的item数量。默认为21。例如一个屏幕能显示8个item,那么默认情况下,屏幕上下各缓存10*8个item, 减少该数字能减小内存消耗并提高性能,但是快速滚动列表时,遇到未渲染的空白view几率增大。这里要注意,因为只有当列表停止滚动时才会更新渲染区域,所以只要item足够多,一直滚动不要停止就一定能看到空白view。
maxToRenderPerBatch: 每批次渲染的item个数,默认为10. 例如一个屏幕能显示8个item, 列表停止时默认情况下需要缓存屏幕上下各80个item, 那么需要16个批次才能完成,如果列表停留时间不够用户马上又继续滚动,因为此时缓存的item数量还不够,可能出现滚不动的现象。 如果该值变大则会使所需批次减少,缓存足够item所需时间减小,用户体验更好。 但是如此js一个事件循环任务过多可能导致其他的如列表响应问题。 有时候设置该值是必要的,比如一个长列表,每屏幕能显示下20个item,那么默认情况maxToRenderPerBatch为10就显得太小,滑动时很容易出现滑不动现象,可以适当放大该值。
removeClippedSubviews: 剪切子视图,移除屏幕外较远位置的所有item,优化内存。iOS上面有bug,安卓默认开启。 主要是在ListView时期长列表优化内存使用。
10、hook自定义组件
例如我项目中自定义了个button组件
export const Button = memo((props) => {
let children = props.children;
let { disabled, loading, style, onPress } = props;
if (typeof children == 'string') {
children = {children}
}
let defaultStyle = {
height: 45, marginLeft: 15, marginRight: 15, alignItems: 'center', justifyContent: 'center',
backgroundColor: ColorConf.main(), borderRadius: 5, opacity: loading || disabled ? 0.5 : 1, flexDirection: 'row'
}
if (style) {
defaultStyle = { ...defaultStyle, ...style }
}
return (
!disabled && onPress && onPress()}>
{loading ? : null}
{children}
)
})
用memo包裹起来跟class时代的pure组件差不多,每次会对传入的props进行浅比较,若不一致才会更新组件
如果像这样使用,那么每当父组件刷新时,由于传入Button的style是一个临时对象,Button会随着父组件一同刷新,显然是不合适的
同上面,应该如下使用
const _loginInWithPassword = useCallback(() => console.log('点击登陆') },[])
const styles = StyleSheet.create({
buttonStyle: { marginTop: 50 }
});
使用useCallback,useMemo等缓存函数,组件等的时候要注意设置好依赖,否则可能出现值捕获等隐性问题
11、使用Fragment
Fragment和View都可以包裹子元素,但是前者不对应具体的视图,仅仅是代表可以包装而已,跟空的标识符一样
<>
>
如上,前面两个完全一样,原生端只存在ChildA和ChildB两个组件。最后那个不一致,对应原生端为View父视图包含ChildA和ChildB两个个组件
视图层级关系减少有利于视图渲染