性能概览
使用 React Native 而非基于 WebView 的工具的一个令人信服的理由是它能达到至少每秒 60 帧,并为你的应用程序提供原生观感。在可行的情况下,我们致力于让 React Native 自动处理优化,让你能专注于应用程序而无需担心性能问题。然而,在某些领域我们尚未达到这种水平,而在其他领域(类似于直接编写原生代码),React Native 无法为你确定最佳优化方法。在这种情况下,手动干预是必要的。我们努力默认提供流畅的用户界面性能,但有时可能无法实现。
本指南旨在教你一些基础知识,帮助你解决性能问题,并讨论常见问题来源及其建议解决方案。
关于帧你需要了解的内容
你的祖父母一代称电影为“会动的画面”是有原因的:视频中逼真的运动是一种错觉,通过以一致的速度快速切换静态图像来创建。我们将这些图像中的每一个称为帧。每秒显示的帧数直接影响视频(或用户界面)的流畅度和最终的逼真度。iOS 和 Android 设备每秒显示至少 60 帧,这让你和 UI 系统最多有 16.67 毫秒来完成生成用户在该间隔内在屏幕上看到的静态图像(帧)所需的所有工作。如果你无法在分配的时间内完成生成该帧所需的工作,那么你将“丢帧”,并且 UI 会显得没有响应。
现在为了稍微混淆一下,打开你应用中的开发菜单并切换`显示性能监视器`。你会注意到有两种不同的帧率。
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` 文件,如下所示:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
这将自动移除项目发布(生产)版本中的所有 console.*
调用。
即使您的项目中没有调用 console.*
,也建议使用此插件。第三方库也可能调用它们。
FlatList
渲染太慢或大型列表滚动性能不佳
如果您的 FlatList
渲染速度很慢,请务必实现 getItemLayout
以通过跳过对渲染项的测量来优化渲染速度。
还有其他第三方列表库针对性能进行了优化,包括 FlashList 和 Legend List。
由于 JavaScript 线程同时处理大量工作而导致 JS 线程 FPS 下降
“导航器过渡缓慢”是这种情况最常见的表现,但也有其他情况可能发生。使用`InteractionManager`可能是一个好方法,但如果延迟动画期间的工作的用户体验成本太高,那么您可能需要考虑`LayoutAnimation`。
`Animated API` 目前在 JavaScript 线程上按需计算每个关键帧,除非你设置 `useNativeDriver: true`,而`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` 中
function handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}