性能概述
使用 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 仅适用于 fire-and-forget 动画(“静态”动画)——如果必须可中断,则需要使用
Animated
。
在屏幕上移动视图(滚动、平移、旋转)会降低 UI 线程 FPS
当你的文本具有透明背景并位于图像顶部时,或者在任何其他需要在每一帧重新绘制视图的 alpha 合成情况下,尤其如此。你会发现启用 shouldRasterizeIOS
或 renderToHardwareTextureAndroid
可以显着帮助解决此问题。
小心不要过度使用它,否则你的内存使用量可能会急剧增加。在使用这些属性时,请分析你的性能和内存使用情况。如果你不打算再移动视图,请关闭此属性。
动画化图像大小会降低 UI 线程 FPS
在 iOS 上,每次你调整 Image
组件的宽度或高度时,都会从原始图像重新裁剪和缩放它。这可能非常耗费资源,尤其是对于大型图像。相反,请使用 transform: [{scale}]
样式属性来动画化大小。你可能这样做的一个例子是当你点击图像并将其放大到全屏时。
我的 TouchableX 视图不是很灵敏
有时,如果我们在同一帧中执行操作,即我们正在调整响应触摸的组件的不透明度或高亮,那么我们只有在 onPress
函数返回后才能看到该效果。如果 onPress
执行 setState
导致大量工作和一些帧丢失,则可能会发生这种情况。解决此问题的方法是将 onPress
处理程序内的任何操作包装在 requestAnimationFrame
中
handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
Navigator 过渡缓慢
如上所述,Navigator
动画由 JavaScript 线程控制。想象一下“从右侧推入”的场景过渡:在每一帧中,新场景都从右向左移动,从屏幕外开始(假设 x 偏移量为 320),最终在场景位于 x 偏移量为 0 时稳定下来。在此过渡期间的每一帧中,JavaScript 线程都需要向主线程发送新的 x 偏移量。如果 JavaScript 线程被锁定,它将无法执行此操作,因此在该帧上不会发生更新,并且动画会卡顿。
对此的一个解决方案是允许将基于 JavaScript 的动画卸载到主线程。如果我们要使用这种方法执行与上面示例中相同的操作,我们可能会在开始过渡时计算新场景的所有 x 偏移量的列表,并将它们发送到主线程以进行优化执行。现在 JavaScript 线程已从这项责任中解放出来,即使它在渲染场景时丢弃了几个帧也没什么大不了的——你可能甚至不会注意到,因为你会被漂亮的过渡分散注意力。
解决这个问题是新的 React Navigation 库背后的主要目标之一。React Navigation 中的视图使用原生组件和 Animated
库来提供至少 60 FPS 的动画,这些动画在原生线程上运行。