性能概览
使用 React Native 而非基于 WebView 的工具的一个令人信服的理由是能够达到至少每秒 60 帧,并为你的应用提供原生的外观和感觉。在可行的情况下,我们希望 React Native 能自动处理优化,让你专注于应用开发,而无需担心性能问题。然而,在某些方面我们尚未完全达到这一水平,在另一些方面(类似于直接编写原生代码),React Native 无法为你确定最佳的优化方案。在这种情况下,就需要手动干预。我们努力默认提供流畅如丝的 UI 性能,但也可能存在无法实现的情况。
本指南旨在教授一些基础知识,帮助你排查性能问题,并讨论常见问题来源及其建议的解决方案。
你需要了解的关于帧的知识
你的祖父母那一代人称电影为“活动照片”是有原因的:视频中的逼真运动是快速、恒定地切换静态图像创造出的错觉。我们将这些图像中的每一个称为帧。每秒显示的帧数直接影响视频(或用户界面)看起来有多流畅和逼真。iOS 和 Android 设备每秒至少显示 60 帧,这意味着你和 UI 系统最多有 16.67 毫秒来完成生成用户在该间隔内将在屏幕上看到的静态图像(帧)所需的所有工作。如果你无法在规定的时间内完成生成该帧所需的工作,那么你就会“丢帧”,UI 就会显得无响应。
现在稍微混淆一下问题,打开应用中的开发者菜单,并切换 Show Perf Monitor
。你会注意到有两种不同的帧率。
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 线程,但 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
。
除非你设置 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
中
function handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}