使用 React Native 的一个重要原因就是达到 60FPS 的刷新,这看起来跟本地 APP 是一样的。在可能的情况下,我们尽量完善 ReactNative 的性能,使你只关注 APP 的逻辑,而可以不用管性能的优化。但是有的地方,我们还没有关注到。同样,跟本地代码 (Object c) 一样,我们不能确定哪种方式是最好的,所以还需要你手动干预。
这个指南的目的是教会你一些基础知识,以帮助您解决性能的问题,并且,讨论常见的一些性能问题以及解决的方法。
在你的祖父辈,它们一般把视频称为 "移动的图片" 的一个原因是:视频中逼真的动作是以固定的速度,快速改变静态的图片创造出来一个错觉。这里的每一个图片就一个是帧。每一秒中图片的数量 (帧),它直接影响了视频的真时性(或者 app 中的用户界面). iOS 的设备是每秒 60 帧。它也就给了你以及 UI 系统大约 16.67 毫秒的时间,来完成生成一张静态图片(帧) 的所有工作。如果你不能在 16.67ms 内完成,则你将会丢失一个帧,UI 会出现无响应状态。
在你的应用程序中,打开 Deveroper 菜单,并切换到 fps 监视器,你会发现有两种不同的帧速率。
许多的 React Native 应用,你的业务逻辑都是运行在 Javascript 线程上的。比如 React 应用的生命周期 (mount, update), API 调用,触摸事件的处理等。并且在每一帧失效前, 更新原生支持的视图(Views) 是被成批处理的,并且在每一次事件循环遍历结束后,发送给本地端。如果 Javascript 线程对于一个帧没有响应,则被认为删除这个帧。举例来说,在一个复杂的应用的 root 组件中,你调用了
. 它会导致重新宣染子组件,这个过程很消耗资源。可以假设为它将花费 200ms, 那么导致 12 帧丢弃 (200ms / 16.67ms)。通过 Javascript 控制的作何动画,在这个过程会被冻结。如果作何计算超过 100ms, 用户就可以感受到。
- this.setState
这通常发生在导航的切换中: 当你添加一个新的路由,Javascript 线程需要读取这个场景所需要的所有组件,然后通过适当的命令发送给本地端,创建视图。这个过程会花费多个帧,引起卡顿,这是因为 transition 是由 Javascript 控制的。有此组件会在 componentDidMount 中做额外的计算,这可能会导航在 transition 卡顿的第二个原因.
另一个例子是响应触摸:如果你要做的任务在 Javascript 线程上跨越多个帧,你可能会注意到 TouchableOpacity 的延迟。这就是在 Javascript 线程在忙的时候,不能处理从主线程发送过来的触摸事件的原因,所以会出现,native view 调整了透明度,而又不对触摸事件做出响应。
许多人可能注意到,使用 NavigationIOS 的性能要比 Navigator 要好。这个原因是,transition(转场动画) 的完成是在 main thread 中完成的。
同样,当 Javascript 被锁上时,你依然可以通过 ScrollView 向上和向下滚动,这是因为 ScrollView 存在于主线程中 (虽然 scroll event 会向 JS 触发,但是对于滚动的发生是没有必要的).
当运行一个打包好的 app, Console.log 语句会引起很大的瓶颈。它会包含调试库 redux-logger, 所以在打包前,确保删除了 Console.log 语句.
在 dev 模式中,会影响到 Javascript 线程的性能. 比如在运行时,向你提供警告和错误信息,检验 propTypes。
上面提供过,Navigator 动画是由 Javascript 线程控制。假设一个从右到左的场景转换, 既添加一个新页面:新的场景 scene, 是从右到左移动 (-320 到 0),在这个转换过程的每一帧,Javascript thread 需要将新的 x 位置发送给主线程。如果 javascript 线程被冻结。它就不能做这些,那么这些帧就不会被更新,动画就变得断断续续。
一劳永逸的解决方案是将基于 Javascript 的动画转变为基于 main thread 的动画。在上面的例子中,我们可能需要计算 transition 的每一个 x 偏移位置,然后发送给主线程,以一个优化的方式来执行。现在 Javascript 不需要在负责这个。
可是这中方案还没有实现,所以在这段期间,我们应该使用 InteractionManager,为新的 scene 选择最少的内容数量以及动画过程。"`InteractionManager.runAfterInteractions 接受一个唯一的回调函数作为参数。这个回调函数在导航动画完成后触发。
你的 scene 组件应该如下
- class ExpensiveScene extends React.Component {
- constructor(props, context) {
- super(props, context);
- this.state = {
- renderPlaceholderOnly: true
- };
- }
- componentDidMount() {
- InteractionManager.runAfterInteractions(() = >{
- this.setState({
- renderPlaceholderOnly: false
- });
- });
- }
- render() {
- if (this.state.renderPlaceholderOnly) {
- return this._renderPlaceholderView();
- }
- return ( < View > <Text > Your full view goes here < /Text>
- </View > );
- }
- _renderPlaceholderView() {
- return ( < View > <Text > Loading... < /Text>
- </View > );
- }
- };
在这,你可以不局限于加载器,也可以是读取部分的类容。比如,当你加载 Facebook appp 时,你会看到新闻评论的占位符是一个灰色的矩形.
我们可以通过以下的几个方面,改善部分性能
initialListSize
这个属性用来指定我们第一次渲染时,要读取的行数。如果我们想尽可能的快,我们可以设置它为 1, 然后可以在后续的帧中,填弃其它的行。每一次读取的行数,由 pageSize 决定.
**pageSize**pageSize
在使用了 initialListSize 之后,ListView 根据 pageSize 来决定每一帧读取的行数,默认值为 1, 但如果你的的 views 非常的小,并且读取时占的资源很少, 你可以调整这个值,在找到适合你的值。
scrollRenderAheadDistance
"以像素为单位,如何预读取要加载的行?"
如果我们的列表有 2000 个项,而让它一次性读取,它会导致内存和计算资源的耗尽。所以 scrollRenderAhead distance 可以指定,超出当前视口多省,继续宣染。
removeClippedSubviews
"当它设置为 true 时,当本地端的 superview 为 offscreen 时 ,不在屏幕上显示的子视图 offscreen(它的 overflow 的值为 hidden) 会被删除。它可以改善长列表的滚动的性能,默认值为 true.
这对于大的 ListViews 来说是一个非常重要。在 Android, overflow 的值通常为 hidden. 所以我们并不需要担心它的设置,但是对于 iOS 来说,你需要设置 row container 的样式为 overflow: hidden
我的组件读取时非常慢,并且不需要立即读取所有的内容
对于这个问题,我们首先可能不会想到使用 ListView. 但使用 ListView 得当,往往是实现移定性能的关键,如我们上面讨论的,ListView 向我们提供了不同的工具,可以让你视图分隔为多个帧中读取,满足特定的需求。请记住 ListView 也可以是水平读取。
如果你使用了 ListView, 你必须提供一个 rowHasChanged 函数,它可以确定是否需要重新渲染一行。如果你使用的是不变的数据结构,它可以跟引用类型的相等比较一样简单。
相似的,你也可以实现 shouldComponentUpdate,以确定需要重新渲染时的条件。如果你写了一个纯组件 (render 函数完成依赖 props 和 state), 你可以利用 PureRenderMixin 来做这个事情,再一次,不可变的数据结构非常有用,可以保持它的快速性,如果你要深入的比较一个大的对像列表,使得重新渲染整个组件会更快,而且只要更少的代码.
"慢的导航转场" 是这个问题的最常见的表现,但也有其它的情形。使用 InteractionManager 是一个最好的方法。但如果在动画期间,有大量的延迟类的工作,则可以考虑 LayoutAnimation.
Animated api 当前计算每一帧都是基于 Javascript 线程,而 LayoutAnimation 利用了核心动画,不会受到 JS 线程和主线程丢帧的影响。
常见的一种情况是弹出对话框的动画 (从上向下滑动,并且淡入的动画), 而初始化和接收多个网络请求的响应,渲染对话框的的内容,并且当打开对话框时,更新视图。可以查看 Animations 指南了解更多使用 LayoutAnimation 的信息.
警告 - LayoutAnimtion 仅适用于 fire-and-forget 动画 ("静态" 动画) - 如果动画需要被中断,则你需要使用 Animated.
这种情况常见于,带透明背景的文本,在一个图片之上。或者它的 alpha 混合的情况。可以使用 shouldRasterizeIOS 和 renderToHardwareTextureAndroid 来改进性能。
但最好不要浪用它,或者你的内存会被用完,在使用这个属性时,最好监控性能和内存的使用。如果你不计划移动一个 View, 则把这些属性关闭。
在 iOS, 每一次你调整一个图片组件的宽和高,它都是从原始图片中 re-croped and scaled。这个过程非常昂贵,特别是对于大图来说。代替的, 我们可以使用 transform: [{scale}] 样式属性动画的改变大小,比如轻触一个图片,然后变为全屏。
有时,如果我们在相同的帧里改变透明度和颜色,以响应触摸事情。我们可能在 onPress 返回之后看不到作何的响应。如果 onPress 里有一个 setState, 它引发大量的工作,并且有一些帧被删除掉,这时就会出现这种情况。一种解决方案是,将作何的动画包装在 requestAnimationFrame 处理器中。
- handleOnPress() {
- // Always use TimerMixin with requestAnimationFrame, setTimeout and
- // setInterval
- this.requestAnimationFrame(() = >{
- this.doExpensiveAction();
- });
- }
使用内置的分析器,获得更多关于 Javascript 线程和主线程的信息。
在 iOS 中,instruments 是一个非常好的工具,对于 Android, 则需要学会使用 systrace.
来源: http://lib.csdn.net/article/reactnative/48948