RN性能优化实践

4,867 阅读8分钟

RN工作原理简述

image.png RN的本质是把中间的这个桥Bridge给搭好,让JS和native可以互相调用。RN为我们提供了JS的运行环境,所以前端开发者们只需要关心如何编写JS代码,画UI只需要画到virtual DOM 中,不需要特别关心具体的平台.至于如何把JS代码转成native代码的脏活累活,RN底层全干了.

image.png 无论是事件消息(触摸事件、蓝牙信息等)还是UI更新数据在RN中都是通过Bridge来在Native和JavaScript之间传递,如图所示:RN中消息的传递完全是异步的,RN的通信数据都会被序列化成JSON然后进行传递;因为通信的过程是比较耗时的,所以尽量避免通过通信来实现UI的频繁的更新。如:通过通信控制动画等,因为这样很难保障在16ms内完成通信以及UI渲染,从而导致丢帧和出现卡顿的现象。

RN加载的主要时间消耗

image.png image.png RN的加载流程主要为几个阶段:

  • 初始化RN环境
  • 加载JS Bundle
  • 运行JS Bundle
  • 渲染页面

通过对FaceBook的ios版进行性能测试,得到上面的耗时图可以看到,绿色的JS Init + Require占据了一大半的时间,这部分主要的操作是:初始化JS环境、下载JS Bundle、运行JS Bundle。 JS Bundle 是由 RN 开发工具打包出来的 JS 文件,其中不仅仅包含了RN 页面组件的 JS 代码,还有 react、react-native 的JS代码,还有我们经常会用上的redux等的代码,所以 JS Bundle文件大小是性能优化的瓶颈 假设我们有一个大型App,它囊括了非常多的页面,但是在常规使用中,很多页面甚至都不会被打开,还有一些复杂的配置文件以及很少使用的功能,这些相关的代码,在App启动的时候都是不需要的,那么,我们就可以考虑通过Unbundling拆包来优化性能。目前主流的方法是拆分Bundle包,把框架代码和业务代码单独出来,框架代码非常大,因此要分离出来单独前置加载,而业务代码则变成很小的JS代码单独发布。

RN框架层性能优化实践

RN性能优化--拆包、分包与裁剪

image.png

  • JS Bundle拆包: RN 打包生成的业务包分为两部分的内容,一部分是公共的基础组件、API 包,统称 common 包,一部分是业务的核心逻辑包。在多业务包模式下,可以共享统一的 common 包。在打开业务前优先预加载 common 包,用户打开业务后再加载 bussiness 包。
  • 业务分包: 将业务包按照路由页面和功能分成多个子的业务子包,让首屏业务逻辑包变小,做到按需加载其他业务包,提升首页启动性能。
  • 业务包裁剪: 将业务代码没有用到 React 的 module、API、组件删除,减少业务包大小来提升启动性能。

RN性能优化--预加载

image.png

  1. 对于一些加载较慢的图片,将链接配置到云端后,在合适时机提前预加载到 Fresco 内存,页面打开后 Fresco 会从缓存中直接读取 bitmap。这项操作也可以在上述的网络接口预拉取中进行。

  2. 对于一些对首屏显示影响较大的网络请求,根据配置拉取并缓存到内存,打开业务后优先从缓存中读取网络接口内容并显示。在RN页面的前置页面中当用户点击RN页面的入口时,通过ReactNative-Android框架中提供的logMarker回调中寻找合适的时机解析Intent对象中的路由协议字段,获取RN页面的相关参数,然后通过RN模块的bundleName绑定RN页面接口的functionId,然后根据functionId和解析出来的接口请求参数提前进行网络请求,并将网络请求的结果缓存到内存中,等到RN页面的代码开始执行的时候优先通过jsBridge获取该RN模块的bundleName对应的缓存中读取网络接口的数据,如果获取数据失败再次尝试拉取RN页面的网络接口。通过多次测试取平均值,此项网络接口预取操作,可以将RN页面首开时间减少200ms左右。但是有由于此项操作需要在主站内写入较多的定制逻辑,有一定的风险。

  3. 提前加载好完整的业务包到内存,生成并缓存 ReactInstanceManager 对象,在业务启动时,从内存缓存中获取该对象,并直接运行绑定 rootview,该方案能提升整体的打开速度 30%-50% 左右,手机设备基本都达到秒开,模拟器设备在 2s 内,但这种通过内存换取速度的方法,在业务量大后,很明显是不可取的,所以整包预加载的局限性比较强。

  4. 从时序运行上,除了 core bridge 的初始化外,js 运行到页面显示,实际上也占用了不少时间,在预加载 core bridge 上更近一步,支持预加载 rootview,提前将要渲染页面的 rootview 运行起来缓存在内存,当然这里加载的还是基础模块,在业务打开时,路由触发展示页面即可,可以做到页面无延时打开,但是对内存的开销,比预加载 core bridge 更高。在RN页面的前置页面在线程空闲的时候执行加载RN页面的reactRootView的任务,然后将获取到的reactRootView缓存在内存中,当用户跳转到RN页面时直接从缓存中获取当前页面的reactRootView,然后setContentView(reactRootView)即可以达到秒开的效果。这种一种空间换时间的方式。目前这种预加载方案仅适合可以提前预知当前页面的下一级页面为有限个RN页面且应用对内存开销要求不高的场景。

RN性能优化--首屏渲染

image.png

RN业务层性能优化实践

RN性能优化点分析

进入App后性能优化的点又在哪里呢?

  • UI事件响应 这块内容都发生在Native端,以事件形式传递到JS端,只是一个触发器,优化空间有限

  • UI更新 JS是决定显示什么界面,一般都是由JS端发起UI更新,同时向native端同步大量的数据和UI结构,这类更新会经常出现性能问题,特别是界面复杂、数据变动量大、动画复杂、变动频率高的情况

  • UI事件响应 + UI更新 如果UI更新改动不大,那么问题不大;如果UI事件触发了UI更新,同时逻辑复杂、耗时比较长,JS端和Native端的数据同步可能会出现时间差,由此会引发性能问题

native代码在设备上的运行速度毋容置疑,而JS作为脚本语言,本来就是以快著称,也就是说两边的独立运行都很快,如此看来,性能瓶颈只会出现在两端的通信上,但两边其实不是直接通信的,而是通过Bridge做中间人,查找、调用模块、接口等操作逻辑,会产生到能让UI层明显可感知的卡顿,那么性能控制就变成了如何尽量减少Bridge所需要的逻辑。

RN性能优化--依赖懒加载

image.png

RN页面渲染生命周期

image.png 根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

采用静态容器减少过度渲染

image.png

采用缓存元素减少过度渲染

image.png

采用代码控制减少过度渲染

image.png

  • 通过浅比较减少过度render
  • 精细化控制需要render的组件
  • 不可变对象(immutable)

组件优化

image.png

组件优化--FlatList

FlatList快速滑动的时候渲染窗口会一直渲染item,这样的操作极大地消耗性能从而造成页面卡顿,为了解决这种卡顿我们可以在onViewableItemsChanged方法回调用将item状态发生变化时的渲染方式做一些优化。 image.png 使用FlatList的注意事项:

  • 渲染窗口中的所有Item在任何props改变时都会重新渲染。
  • 当某行滑出渲染区域之外后,其内部状态将不会保留。
  • 此组件继承自PureComponent而非通常的Component,这意味着如果其props在浅比较中是相等的话,则不会重新渲染。
  • 为了优化内存占用同时保持滑动的流畅,列表内容会在屏幕外异步绘制。
  • 如果你有一些特殊的需求或用例,你也通过调整一些参数来实现。

动画

image.png

异步

image.png

其他

image.png

RN性能优化点

image.png

参考

1、案例:React Native在字节跳动游戏营销场景中的实践(mp.weixin.qq.com/s/KYt33q2H8…

2、深入剖析React Native下一代架构重构(blog.51cto.com/u_15057848/…

3、携程大规模应用RN的工程化实践(mp.weixin.qq.com/s/Z1GUJW3qB…

4、React Native拆包及热更新方案(solart.cc/2017/02/22/…

5、react组件性能优化探索实践(imweb.io/topic/57751…

6、React Native 性能优化 (官网指南搬运)(juejin.cn/post/684490…

7、React Native 拆包原理和实践(cloud.tencent.com/developer/a…