跳到主要内容

性能概览

使用 React Native 而非基于 WebView 的工具的一个令人信服的原因是,它能实现至少每秒 60 帧,并为您的应用提供原生外观和感觉。只要可行,我们的目标是让 React Native 自动处理优化,让您能够专注于您的应用而无需担心性能。然而,在某些方面我们尚未达到这个水平,而在其他方面,React Native(类似于直接编写原生代码)无法为您确定最佳优化方法。在这种情况下,需要手动干预。我们力求默认提供流畅的 UI 性能,但有时可能无法实现。

本指南旨在教您一些基础知识,以帮助您排查性能问题,并讨论常见问题来源及其建议解决方案

您需要了解的帧相关知识

您的祖父母那一代人称电影为“动态图片”是有原因的:视频中逼真的运动是一种通过以恒定速度快速切换静态图像而产生的幻觉。我们称这些图像中的每一个为帧。每秒显示的帧数直接影响视频(或用户界面)的流畅度和最终逼真度。iOS 和 Android 设备每秒显示至少 60 帧,这意味着您和 UI 系统最多有 16.67 毫秒来完成生成用户在该时间间隔内将在屏幕上看到的静态图像(帧)所需的所有工作。如果您无法在分配的时间内完成生成该帧所需的工作,那么您将“掉帧”,并且 UI 将显得无响应。

现在,为了让事情稍微复杂一些,打开您应用中的开发者菜单,然后切换Show Perf Monitor。您会注意到有两种不同的帧率。

Performance Monitor screenshot

JS 帧率(JavaScript 线程)

对于大多数 React Native 应用程序,您的业务逻辑将在 JavaScript 线程上运行。这是您的 React 应用程序所在的位置,API 调用在此处进行,触摸事件在此处处理等等。对原生支持的视图的更新在事件循环的每次迭代结束时批量发送到原生端,在帧截止日期之前(如果一切顺利)。如果 JavaScript 线程在一帧内无响应,则将被视为掉帧。例如,如果您在一个复杂应用程序的根组件上设置了一个新状态,并且导致重新渲染计算开销大的组件子树,那么这可能需要 200 毫秒,并导致 12 帧掉帧。在此期间,由 JavaScript 控制的任何动画都将显得冻结。如果掉帧足够多,用户就会感觉到。

一个例子是响应触摸:如果您在 JavaScript 线程上跨多帧进行工作,您可能会注意到响应 TouchableOpacity 等会出现延迟。这是因为 JavaScript 线程正忙,无法处理从主线程发送过来的原始触摸事件。因此,TouchableOpacity 无法响应触摸事件并命令原生视图调整其不透明度。

UI 帧率(主线程)

您可能已经注意到,原生堆栈导航器(例如 React Navigation 提供的 @react-navigation/native-stack)的性能开箱即用优于基于 JavaScript 的堆栈导航器。这是因为过渡动画在原生主 UI 线程上执行,因此它们不会被 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 文件

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

这将自动移除您项目发布(生产)版本中的所有 console.* 调用。

即使您的项目中没有进行 console.* 调用,也建议使用此插件。第三方库也可能调用它们。

FlatList 渲染速度太慢或大型列表滚动性能不佳

如果您的 FlatList 渲染缓慢,请确保您已实现 getItemLayout 以通过跳过对渲染项的测量来优化渲染速度。

还有其他第三方列表库经过性能优化,包括 FlashListLegend List

由于 JavaScript 线程同时处理大量工作而导致 JS 线程 FPS 下降

“导航器过渡缓慢”是这种情况最常见的表现,但也有其他情况可能发生。使用 InteractionManager 可能是一个不错的方法,但如果用户体验成本太高,无法在动画期间延迟工作,那么您可能需要考虑 LayoutAnimation

除非您设置 useNativeDriver: true,否则 Animated API 当前在 JavaScript 线程上按需计算每个关键帧,而 LayoutAnimation 利用 Core Animation,不受 JS 线程和主线程掉帧的影响。

使用此方法的一个例子是动画显示一个模态(从顶部滑下并淡入半透明叠加层),同时初始化并可能接收多个网络请求的响应,渲染模态内容,并更新打开模态的视图。有关如何使用 LayoutAnimation 的更多信息,请参阅动画指南

注意事项

  • LayoutAnimation 仅适用于“即发即忘”动画(“静态”动画)——如果它必须可中断,您将需要使用 Animated

在屏幕上移动视图(滚动、平移、旋转)导致 UI 线程 FPS 下降

在 Android 上尤其如此,当您的文本带有透明背景并位于图像之上,或任何其他需要在每帧重新绘制视图时需要进行 Alpha 混合的情况。您会发现启用 renderToHardwareTextureAndroid 可以显著改善这种情况。对于 iOS,shouldRasterizeIOS 默认已启用。

注意不要过度使用此功能,否则您的内存使用量可能会飙升。使用这些属性时,请对您的性能和内存使用情况进行分析。如果您不打算再移动视图,请关闭此属性。

动画化图像大小导致 UI 线程 FPS 下降

在 iOS 上,每次调整 Image 组件的宽度或高度时,它都会从原始图像重新裁剪和缩放。这可能会非常昂贵,尤其是对于大型图像。相反,请使用 transform: [{scale}] 样式属性来动画化大小。您可能会这样做的一个例子是当您点击图像并将其放大到全屏时。

我的 TouchableX 视图响应不灵敏

有时,如果我们同时在一个帧中执行操作并调整响应触摸的组件的不透明度或高光,那么在 onPress 函数返回之前,我们将看不到该效果。如果 onPress 设置了一个状态,导致大量重新渲染并因此掉帧,则可能会发生这种情况。一个解决方案是将 onPress 处理程序中的任何操作包装在 requestAnimationFrame

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