总结RN开发中一些优化

1,746 阅读6分钟

requestAnimationFrame(fn)

requestAnimationFrame(fn)会在每帧刷新之后执行一次

InteractionManager(交互管理器)

原生应用感觉如此流畅的一个重要原因就是在互动和动画的过程中避免繁重的操作。在React Native里,我们目前受到限制,因为我们只有一个JavaScript执行线程。不过你可以用InteractionManager来确保在执行繁重工作之前所有的交互和动画都已经处理完毕。 应用可以通过以下代码来安排一个任务,使其在交互结束之后执行:

InteractionManager.runAfterInteractions(() => { // ...需要长时间同步执行的任务... });

我们来把它和之前的几个任务安排方法对比一下: requestAnimationFrame(): 用来执行在一段时间内控制视图动画的代码 setImmediate/setTimeout/setInterval(): 在稍后执行代码。注意这有可能会延迟当前正在进行的动画。 runAfterInteractions(): 在稍后执行代码,不会延迟当前进行的动画。 触摸处理系统会把一个或多个进行中的触摸操作认定为'交互',并且会将runAfterInteractions()的回调函数延迟执行,直到所有的触摸操作都结束或取消了。 InteractionManager还允许应用注册动画,在动画开始时创建一个交互“句柄”,然后在结束的时候清除它。

var handle = InteractionManager.createInteractionHandle(); // 执行动画... (runAfterInteractions中的任务现在开始排队等候) // 在动画完成之后 InteractionManager.clearInteractionHandle(handle); // 在所有句柄都清除之后,现在开始依序执行队列中的任务

RN的渲染过程

RN运行过程中产生4个线程

  1. UI线程: 也即主线程。RN最终调用系统的渲染能力,所以RN最终的渲染是在主线程,这也是Native的渲染线程。

  2. JS线程: JavaScript代码执行的线程。比如逻辑代码运行,触摸事件处理,在JS的每个事件循环结束后向Native批量更新视图等。

  3. Shadow线程: Virtual Dom Tree生成后批量发送到时Shadow线程,该线程创建相对应的ShadowTree,并计算布局,布局结束后发送而已信息到UI线程,更新UI

  4. Native Modules 线程: 一些获取平台API代码在这里执行。

具体过程

  1. 主线程加载JS包资源
  2. 当主线程成功加载完JS包资源,调用JS线程执行JS代码
  3. JS生成或者更新Virtual DOM Tree,并发送到时 Shadow 线程
  4. Shadow线程完成布局并将布局信息发送到时主线程
  5. 主线程渲染

RN性能优化

UI我们优化的要求就是能够达到60fps,这样每帧的间隔不能超过16ms,由于RN的整个渲染过程是异步的,这首先就要求RN产生Virtual Dom Tree的时间间隔在保持在16ms之内,也即JS线程每个事件循环的处理不能超过16ms

生产环境测试性能

测试性能一定要在生产环境下,开发环境下会产生额外代码,比如error,waring输出,类型检查等

移除console.*

CRN打包时会自动移除

大列表用

FlatList不会全量渲染,内存及性能比较好

依赖懒加载

在框架执行编写好的业务代码前,需要把在内存中加载并解析代码,代码量越大这个过程就更耗时,导致首屏渲染速度过慢。而且往往会出现一些页面或者组件根本不会被用户访问到。这时可以通过懒加载来优化。 官网有给出例子

VeryExpensive.js

import React, { Component } from 'react';
import { Text } from 'react-native';
// ... import some very expensive modules

// You may want to log at the file level to verify when this is happening
console.log('VeryExpensive component loaded');

export default class VeryExpensive extends Component {
  // lots and lots of code
  render() {
    return <Text>Very Expensive Component</Text>;
  }
}

Optimized.js

import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';

let VeryExpensive = null; //定义变量

export default class Optimized extends Component {
  state = { needsExpensive: false }; // 定义内部状态来控制组件是否需要加载

  didPress = () => {
    // 在触发需要加载组件的事件时
    if (VeryExpensive == null) { // 不重复引用
      VeryExpensive = require('./VeryExpensive').default;  // 把组件的引用赋给定义好的变量
    }

    this.setState(() => ({
      needsExpensive: true, // 更改控制的状态,触发组件re-render
    }));
  };

  render() {
    return (
      <View style={{ marginTop: 20 }}>
        <TouchableOpacity onPress={this.didPress}>
          <Text>Load</Text>
        </TouchableOpacity>
        {this.state.needsExpensive ? <VeryExpensive /> : null}
      </View>
    );
  }
}

优化组件渲染次数

React 在内部state 或者外部传入的props 发生改变时,会重新渲染组件。如果在短时间内有大量的组件要重新渲染就会造成严重的性能问题。这里有一个可以优化的点。

  • 使用PureComponent 让组件自己比较props 的变化来控制渲染次数,实践下来这种可控的方式比纯函数组件要靠谱。或者在Component 中使用 shouldComponentUpdate 方法,通过条件判断来控制组件的更新/重新渲染。

  • 使用PureComponent 时要注意这个组件内部是浅比较状态,如果props 的有大量引用类型对象,则这些对象的内部变化不会被比较出来。所以在编写代码时尽量避免复杂的数据结构

  • 细粒度组件,拆分动态/静态组件。将需要频繁刷新的部分拆成最小化的动态组件,并靠近叶子结点(处在树的末端)

首屏优化

首屏只渲染首屏可见部分,接着再做全量渲染

异步,回调

JavaScript 单线程,要利用好它的异步特性,和一些钩子回调。 比如上面提到路由切换时componentDidMount 中的操作会导致卡顿,这里可以使用 InteractionManager.runAfterInteractions() 将需要执行的操作放到runAfterInteractions 的回调中执行。

componentDidMount() {
	InteractionManager.runAfterInteractions(() => {
        // your actions
	})
}

复制代码需要注意的是 InteractionManager 是监听所有的动画/交互 完成之后才会触发 runAfterInteractions 中的回调,如果项目中有一些长时间动画或者交互,可能会出现长时间等待。所以 由于 InteractionManager 的不可控性,使用的时候要根据实际情况调整。 在react-native 中的一些动画反馈,比如TouchableOpacity 在触摸时会响应 onPress 并且 自身的透明度会发生变化,这个过程中如果 onPress 中有复杂的操作,很可能会导致组件的透明反馈卡顿,这时可以将onPress 中的操作包裹在 requestAnimationFrame 中。这里给出一个我的实践(利用styled-component)

import styled from 'styled-components'

export const TouchableOpacity = styled.TouchableOpacity.attrs({
  onPress: props => () => {
    requestAnimationFrame(() => {
      props.onPressAsync && props.onPressAsync()
    }, 0)
  }
})

复制代码这里把onPress 改成在 requestAnimationFrame 的回调中执行onPressAsync 传入的操作。 同理,还在FlatList 的onReachEnd实践了这个操作,来避免iOS 中滚动回弹时执行操作的卡顿。 以上

组件复用

同级的一组组件,可考虑提供key,使RN 在diff的过程有较高的复用率

RN容器预加载

尽量减少层级

在xcode中检查RN生成的View Tree,我们发现层级普遍比较多,比较深。

原生开发中,只要写好约束,可以比较好地控制好层级深度。

RN中提供的布局没有约束强大,一个界面的生成,不得不嵌套很多布局容器,这也导致在Native端View Tree比较深,嵌套比较多。

实际开发中,要优化布局,尽量减少比较深的层级

Fragment可以减少不必要的DOM结点

尽量使用Text组件嵌套完成文字的布局

RecyleListView

极为高效的复用,瀑布流的支持

长列表中,图片优化很重要

RN没有提供内存缓存图片,只有文件缓存

长列表,不在屏幕范围的图片,卸载有助官方性能提升