性能概述
使用 React Native 而不是基于 WebView 的工具的一个重要原因是,它能够达到至少每秒 60 帧的刷新率,并为您的应用提供原生外观和体验。在可行的情况下,我们旨在让 React Native 自动处理优化,让您专注于应用开发,而无需担心性能问题。但是,在某些领域我们还没有完全达到这个水平,而在其他一些领域,React Native(类似于直接编写原生代码)无法确定最适合您的优化方法。在这种情况下,需要进行手动干预。我们力求默认提供流畅的 UI 性能,但在某些情况下可能无法实现。
本指南旨在教授您一些基础知识,帮助您排查性能问题,并讨论常见性能问题的来源及其建议的解决方案。
关于帧你需要了解的内容
您祖父母那一辈的人称电影为"活动图像"是有原因的:视频中的逼真运动是通过以恒定速度快速切换静态图像产生的幻觉。我们将这些图像中的每一个称为帧。每秒显示的帧数直接影响视频(或用户界面)看起来有多流畅,以及最终的真实感。iOS 设备至少以每秒 60 帧的速度显示,这为您和 UI 系统提供了最多 16.67 毫秒的时间来完成生成静态图像(帧)所需的所有工作,用户将在该时间间隔内在屏幕上看到该图像。如果您无法在分配的时间段内完成生成该帧所需的工作,那么您将“丢失一帧”,UI 将显得无响应。
现在,为了让事情稍微复杂一点,请打开应用中的开发菜单并切换 显示性能监控器
。您会注意到有两个不同的帧率。
JS 帧率(JavaScript 线程)
对于大多数 React Native 应用来说,您的业务逻辑将在 JavaScript 线程上运行。这是您的 React 应用所在的位置,API 调用在此处发出,触摸事件在此处处理,等等…对原生支持视图的更新会批量处理,并在事件循环的每次迭代结束前(如果一切顺利)发送到原生端,并在帧截止日期之前发送。如果 JavaScript 线程在一帧内无响应,则将被视为丢帧。例如,如果您要对复杂应用的根组件调用 this.setState
,并且它导致重新渲染计算量大的组件子树,则可以想象这可能需要 200 毫秒,并导致 12 帧丢失。在此期间,任何由 JavaScript 控制的动画似乎都会冻结。如果任何操作花费超过 100 毫秒,用户就会感觉到。
这通常发生在 Navigator
过渡期间:当您推送新路由时,JavaScript 线程需要渲染场景所需的所有组件,以便向原生端发送正确的命令来创建支持视图。此处完成的工作通常需要几帧并导致卡顿,因为过渡由 JavaScript 线程控制。有时,组件会在 componentDidMount
中执行其他工作,这可能会导致过渡中出现第二次卡顿。
另一个例子是响应触摸:如果您在 JavaScript 线程上跨多帧执行工作,您可能会注意到响应 TouchableOpacity
的延迟,例如。这是因为 JavaScript 线程很忙,无法处理从主线程发送的原始触摸事件。因此,TouchableOpacity
无法对触摸事件做出反应并命令原生视图调整其不透明度。
UI 帧率(主线程)
许多人注意到 NavigatorIOS
的性能比 Navigator
更好。原因是过渡的动画完全在主线程上完成,因此不会受到 JavaScript 线程上的丢帧的影响。
同样,当 JavaScript 线程被锁定时,您可以愉快地在 ScrollView
中上下滚动,因为 ScrollView
位于主线程上。滚动事件会分派到 JS 线程,但接收这些事件对于滚动发生并非必要。
常见性能问题来源
在开发模式下运行 (dev=true
)
在开发模式下运行时,JavaScript 线程的性能会大大下降。这是不可避免的:在运行时需要完成更多工作才能为您提供良好的警告和错误消息。始终确保在发布版本中测试性能。
使用 console.log
语句
在运行捆绑后的应用时,这些语句可能会导致 JavaScript 线程出现重大瓶颈。这包括来自调试库(例如redux-logger)的调用,因此请确保在捆绑之前删除它们。您还可以使用此Babel 插件,它会删除所有 console.*
调用。您需要先使用 npm i babel-plugin-transform-remove-console --save-dev
安装它,然后像这样编辑项目目录下的 .babelrc
文件
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
这将自动删除项目发布(生产)版本中的所有 console.*
调用。
即使项目中没有进行 console.*
调用,也建议使用该插件。第三方库也可能会调用它们。
ListView
初始渲染太慢或大型列表的滚动性能不佳
改为使用新的FlatList
或SectionList
组件。除了简化 API 外,新的列表组件还具有显著的性能增强,主要体现在无论行数多少,内存使用量几乎恒定。
如果您的FlatList
渲染速度很慢,请确保您已实现getItemLayout
以通过跳过已渲染项的测量来优化渲染速度。
重新渲染几乎不变的视图时 JS FPS 下降
如果您使用的是 ListView,则必须提供一个 rowHasChanged
函数,该函数可以通过快速确定是否需要重新渲染行来减少大量工作。如果您使用的是不可变数据结构,则这只需要进行引用相等性检查。
同样,您可以实现 shouldComponentUpdate
并指示希望组件重新渲染的确切条件。如果您编写纯组件(其中 render 函数的返回值完全依赖于 props 和 state),则可以利用 PureComponent 为您执行此操作。再次强调,不可变数据结构有助于保持速度——如果您必须对大型对象列表进行深度比较,那么重新渲染整个组件可能会更快,而且肯定需要更少的代码。
由于同时在 JavaScript 线程上执行大量工作导致 JS 线程 FPS 下降
"Navigator 过渡缓慢" 是这种情况最常见的表现形式,但在其他情况下也可能发生。使用 InteractionManager 可能是一种不错的方法,但如果用户体验成本过高而无法在动画期间延迟工作,那么您可能需要考虑 LayoutAnimation。
Animated API 目前按需在 JavaScript 线程上计算每个关键帧,除非您设置 useNativeDriver: true
,而 LayoutAnimation 利用 Core Animation,不受 JS 线程和主线程丢帧的影响。
我使用此方法的一个案例是在初始化并可能接收多个网络请求的响应、渲染模态的内容以及更新模态打开位置的视图的同时,为模态设置动画(从顶部向下滑动并淡入半透明叠加层)。有关如何使用 LayoutAnimation 的更多信息,请参阅动画指南。
注意事项
- LayoutAnimation 仅适用于即发即弃动画(“静态”动画)——如果它必须可中断,则需要使用
Animated
。
在屏幕上移动视图(滚动、平移、旋转)导致 UI 线程 FPS 下降
当你在图像上放置具有透明背景的文本,或任何需要 Alpha 合成才能在每一帧重新绘制视图的情况下,尤其如此。你会发现启用 shouldRasterizeIOS
或 renderToHardwareTextureAndroid
可以显著改善这种情况。
注意不要过度使用这些属性,否则你的内存使用量可能会暴涨。在使用这些属性时,请分析你的性能和内存使用情况。如果你不再打算移动某个视图,请关闭此属性。
调整图像大小的动画会降低 UI 线程的 FPS
在 iOS 上,每次调整 Image 组件的宽度或高度时,都会从原始图像重新裁剪和缩放。这可能会非常耗费资源,特别是对于大型图像而言。相反,请使用 transform: [{scale}]
样式属性来为大小设置动画。例如,当你点击图像并将其放大到全屏时,你可能会这样做。
我的 TouchableX 视图响应速度不佳
有时,如果我们在调整正在响应触摸的组件的不透明度或高亮显示的同一帧中执行操作,则在 onPress
函数返回后,我们才会看到该效果。如果 onPress
执行 setState
导致大量工作和几帧掉帧,则可能会发生这种情况。解决此问题的方法是在 onPress
处理程序中将任何操作包装在 requestAnimationFrame
中。
handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
导航器切换缓慢
如上所述,Navigator
动画由 JavaScript 线程控制。想象一下“从右侧推入”的场景切换:每一帧,新场景都从右侧移动到左侧,从屏幕外开始(假设 x 偏移量为 320),最终在场景位于 x 偏移量为 0 时稳定下来。在此切换期间的每一帧,JavaScript 线程都需要向主线程发送新的 x 偏移量。如果 JavaScript 线程被阻塞,则它无法执行此操作,因此该帧上不会发生任何更新,动画会卡顿。
解决此问题的一种方法是允许将基于 JavaScript 的动画卸载到主线程。如果我们要使用这种方法执行与上述示例相同的操作,我们可能会在开始切换时计算新场景的所有 x 偏移量的列表,并将它们发送到主线程以进行优化执行。现在 JavaScript 线程已从这项责任中解放出来,如果它在渲染场景时掉了几帧也没关系——你可能甚至不会注意到,因为你会被漂亮的切换所吸引。
解决这个问题是新的 React Navigation 库的主要目标之一。React Navigation 中的视图使用原生组件和 Animated
库来提供至少 60 FPS 的动画,这些动画在原生线程上运行。