React Native 性能优化浅谈

一、概览

使用 React Native 替代基于 WebView 的框架来开发 App 的一个强有力的理由,就是为了使 App 可以达到每秒 60 帧(足够流畅),并且能有类似原生 App 的外观和手感。因此我们也尽可能地优化 React Native 去实现这一目标,使开发者能集中精力处理 App 的业务逻辑,而不用费心考虑性能。但是,总还是有一些地方有所欠缺,以及在某些场合 React Native 还不能够替你决定如何进行优化(用原生代码写也无法避免),因此人工的干预依然是必要的。

1.1、JS 帧率(JavaScript 线程)

对大多数 React Native 应用来说,业务逻辑是运行在 JavaScript 线程上的。这是 React 应用所在的线程,也是发生 API 调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果 JavaScript 线程有一帧没有及时响应,就被认为发生了一次丢帧。

例如,你在一个复杂应用的根组件上调用了this.setState,从而导致一次开销很大的子组件树的重绘,可想而知,这可能会花费 200ms 也就是整整 12 帧的丢失。此时,任何由 JavaScript 控制的动画都会卡住。只要卡顿超过 100ms,用户就会明显的感觉到。

这是因为 JavaScript 线程太忙了,不能够处理主线程发送过来的原始触摸事件,就不能及时响应这些事件并命令主线程的页面去调整透明度了。

1.2、UI 帧率(主线程)

同样,当 JavaScript 线程卡住的时候,你仍然可以欢快的上下滚动ScrollView,因为ScrollView运行在主线程之上(尽管滚动事件会被分发到 JS 线程,但是接收这些事件对于滚动这个动作来说并不必要)。

二、常见卡顿原因

引起卡顿的原因有很多种,过度绘制,js执行逻辑耗时过久,动画卡顿。内存问题等等。下面是一些常见的问题和解决思路。

2.1、开发模式 (dev=true)

JavaScript 线程的性能在开发模式下是很糟糕的。这是不可避免的,因为有许多工作需要在运行的时候去做,譬如使你获得良好的警告和错误信息,又比如验证属性类型(propTypes)以及产生各种其他的警告。请务必注意在release 模式下去测试性能。

本地调试的时候可以通过设置dev菜单中的Show Perf Monitor设置中的第一个选项dev=false,重启可达到和release几乎同等的效果。

2.2、console.log 语句

在运行打好了离线包的应用时,控制台大量打印语句可能会拖累 JavaScript 线程。注意有些第三方调试库也可能包含控制台打印语句,比如redux-logger,所以在发布应用前请务必仔细检查,确保全部移除。

有个babel 插件可以帮你移除所有的console.*调用。首先需要使用yarn add --dev babel-plugin-transform-remove-console来安装,然后在项目根目录下编辑(或者是新建)一个名为·.babelrc`的文件,在其中加入:

{
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

这样在打包发布时,所有的控制台语句就会被自动移除,而在调试时它们仍然会被正常调用。

2.3、使用动画改变图片的尺寸时,UI 线程掉帧

在 iOS 上,每次调整 Image 组件的宽度或者高度,都需要重新裁剪和缩放原始图片。这个操作开销会非常大,尤其是大的图片。比起直接修改尺寸,更好的方案是使用transform: [{scale}]的样式属性来改变尺寸。比如当你点击一个图片,要将它放大到全屏的时候,就可以使用这个属性。

2.4、 在重绘一个几乎没有什么变化的页面时,JS 帧率严重降低

你可以实现shouldComponentUpdate函数来指明在什么样的确切条件下,你希望这个组件得到重绘。如果你编写的是纯粹的组件(界面完全由 props 和 state 所决定),你可以利用PureComponent来为你做这个工作。再强调一次,不可变的数据结构(immutable,即对于引用类型数据,不修改原值,而是复制后修改并返回新值)在提速方面非常有用 —— 当你不得不对一个长列表对象做一个深度的比较,它会使重绘你的整个组件更加快速,而且代码量更少。

2.5、 Touchable 系列组件不能很好的响应

有些时候,如果我们有一项操作与点击事件所带来的透明度改变或者高亮效果发生在同一帧中,那么有可能在onPress函数结束之前我们都看不到这些效果。比如在onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将onPress处理函数中的操作封装到requestAnimationFrame中:

handleOnPress() {
  requestAnimationFrame(() => {
    this.doExpensiveAction();
  });
}

三、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端通信性能有所损耗,

  6. 响应速度
    由于js是单线程,当在执行一些计算量很大的任务时可能会造成堵塞卡顿现象。此时可以将任务稍微延后执行,避免大量任务在同一个js 事件循环中导致其他任务无法执行。相应的方法有InteractionManager,requestAnimationFrame,setTimeOut(0)等,原理都大同小异

  7. 刷新问题
    每次setState导致的render都会进行一次内存中diff计算,尽管diff效率很高(O(n)),但是还是应该避免不必要的diff。 Pure组件、自定义shouldComponentUpdate实现避免不必要的刷新
    shouldComponentUpdate
    多余提一句,在使用shouldComponentUpdate的时候,要谨慎使用。这个方法就是利用shouldComponentUpdate的消耗来换取render的消耗。

    当某些小的、调用的次数少的component,就没有必要添加shouldComponentUpdate检查。

    当组件够大,够复杂,可以考虑使用这个方法来减少re-render的消耗。当然,还是需要考虑用这个方法的消耗和diff&render的消耗比起来哪个更划算。

  8. 预加载
    对一些重要的,很可能会用到的内容预先加载,例如图片浏览器,当浏览某一张图片时可以预加载前后两张图片,优化用户体验。

  9. FlatList的优化。
    页面中的重头戏FlatList,尽管经过了大量优化,在数据较多时使用还是需要注意的。

    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. 使用Fragment
    Fragment和View都可以包裹子元素,但是前者不对应具体的视图,仅仅是代表可以包装而已,跟空的标识符一样。

    <React.Fragment>
        <ChildA />
        <ChildB />
    </React.Fragment>
    
    <>
        <ChildA />
        <ChildB />
    </>
    
    <View>
        <ChildA />
        <ChildB />
    </View >
    

    如上,前面两个完全一样,原生端只存在ChildA和ChildB两个组件。最后那个不一致,对应原生端为View父视图包含ChildA和ChildB两个个组件,视图层级关系减少有利于视图渲染

  11. redu相关优化

    • 在reducer里面,尽量减少数据的变动,不要做多余、无意义的事。
      也就是能不改变就不改变。比如不要做下面这种无谓的事情:
    function reducer(state,action){
        // ....一大堆逻辑代码
        return {
        ...state
        }
    }
    

    这个代码虽然在selector中,也可以通过areStatePropsEqual来判断计算后的state是否发生了改变。
    但是如果直接return state;就可以直接被areStatesEqual拦截,避免多余的计算和对比。

    • 要做多余的检查
      同样,state内部数据,如果数据相同,尽量使用原数据。只针对复杂数据类型(Object,Array)。
      比如:
    function reducer(state,action) {
        let mayNotChange = state.mayNotChange; // mayNotChange为Array或Object
        let newState = {...mayNotChange};
        // ...一大堆逻辑
        return {
            ...state,mayNotChange: changed ? newState : mayNotChange // 没有发生改变的话,就用原来的对象
        }
    }
    

    很多时候,一般习惯于通过计算,然后直接把生成的newState赋值给mayNotChange。

    由于众所周知的{} !== {}的情况,如果能通过简单判断来决定是否可以选择使用原来的对象,那么就可以通过areStatePropsEqual来进行判断,同样可以避免不必要的计算,更可以避免不必要的渲染。

    注: 所说的选择使用原来的对象,是确定数据没有发生改变的时候,使用原对象。并不是说当发生改变的时候,也在原来的对象上面修改最好。在不考虑自定义areStatesEqualareStatePropsEqual的情况下,如果只在原对象上面进行修改,可能会造成对比的时候,前后两种结果相同,可能造成无法重新渲染的情况

    优化equal的四个方法
    在connect的option中,有四个对比的方法
    areStatesEqual(默认为===),用来判断redux store返回的state是否和之前的相同
    areOwnPropsEqual(默认为shallowEqual),用来判断父组件传入的props是否和之前的相同
    areStatePropsEqual(默认为shallowEqual),用来判断mapStateToProps的结果是否和之前的相同
    areMergedPropsEqual(默认为shallowEqual),用来判断最后merge合并的最终结果是否和之前的相同

    可以通过自己的需求对着四个方法进行优化。
    比如一个redux的state是这个样子:

    state = {
        pageA: {...},pageB: {...},number: 2
    }
    

    而在pageA里面只需要pageA和number,那么就可以通过areStatesEqual来进行对比:

    function areStatesEqual(prev,current){
        return prev.number === current.number && isEqual(prev.pageA,current.pageA);
    }
    

    或者针对复杂结构数据的情况,进行特殊处理,比如深度对比

    function areStatePropsEqual(prev,current){
    return deepEqual(prev,current);
    }
    

    这些优化都可以减少不必要的计算和重渲染。

四、自己项目做过的优化

  1. 优化逻辑减少action
    逻辑通过分析action的行为,来分析一些比较卡顿时候的逻辑,清除一些无用的操作,简化步骤,去除重复的无用的action行为触发。

    dispatch中打印日志,能够较快的分析出界面和功能触发的action行为和整体逻辑。

  2. 进入频道的逻辑变更
    删除一个switch_channel的通知,减少重复逻辑
  3. 网络加载逻辑
    通过对网络请求和当前项目导航框架的分析,结合功能需求和产品的思路,来延迟加载一些界面和请求一些数据,如话题数据,文件数据等采取查看当前页签内容是触发绘制和请求。

    这种数据请求的修改需要注意当前应用其他功能对获取数据的依赖性,若没有怎么处理

  4. 自定义shoulComponentUpdate
    通过重写一些root 组件及较复杂组件的shoulComponentUpdate方法,自定义控制render,来减少自上而下的过度绘制。

    结合redux connect提供的options四个函数areStatePropsEqual等和使用deepEqual等深度比较函数可以合理自定义控制刷新。

  5. 代码优化
    • 通过分析redux跟Component绑定connnect时候返回的props,减少在内部出现重新生成新的对象的可能,因为重新生成对象会触发组件的重新绘制。

      应不在或避免在mapStateToProps中生成新的对象并作为props参数返回去,会引发过度绘制。

    • 根据以上列举,对一些代码进行优化,如react-redux,redux,connect组件时候,尽量返回基本数据类型,这样可以减少对比耗时,避免过多的render。

参考文档:

React-Native性能调优

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Native 是一种基于 React 的跨平台移动应用开发框架,其性能受到多种因素的影响。以下是一些优化 React Native 应用性能的建议: 1. 减少渲染次数。React Native 的渲染是基于虚拟 DOM 的,因此组件的更新会引起重新渲染。减少组件的更新次数可以减少渲染次数。可以使用 shouldComponentUpdate 或 PureComponent 来避免不必要的渲染。 2. 使用 FlatList 或 SectionList。FlatList 和 SectionList 是 React Native 的内置组件,它们可以帮助减少渲染次数和内存使用。它们可以按需渲染列表项,而不是一次性渲染所有列表项。 3. 使用动画。React Native 提供了一些内置的动画组件和 API,可以让应用的界面更加流畅和生动。使用动画时,应尽量避免在渲染期间执行操作,以免影响性能。 4. 使用原生组件。React Native 允许开发者使用原生组件来替代一些 React Native 内置组件,以提高性能。例如,使用原生 ScrollView 替代 React Native 的 ScrollView。 5. 使用性能监测工具。React Native 提供了一些性能监测工具,例如 React Native Performance Monitor 和 Reactotron,可以帮助开发者分析应用的性能瓶颈,从而进行针对性的优化。 6. 使用代码分割。React Native 应用可以使用代码分割来减少应用的首次加载时间。可以使用 React Native 的内置代码分割工具或第三方工具进行代码分割。 7. 使用缓存。React Native 应用可以使用缓存技术来减少网络请求和数据处理的次数,从而提高应用的性能。可以使用内置的 AsyncStorage 或第三方缓存库来实现缓存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值