跳至主内容

性能概述

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

选择 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 线程,但滚动操作本身并不依赖这些事件的接收。

常见性能问题根源

在开发模式下运行 (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 线程同时处理大量任务导致 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 视图响应迟缓

有时,如果我们在响应触摸操作的同时,还在同一帧内调整组件的不透明度(opacity)或高亮状态,那么这些视觉变化效果可能要等到 onPress 函数执行完成后才会显现。这种情况通常发生在 onPress 中触发了导致大量重渲染的状态更新,进而造成丢帧。解决方案是将 onPress 处理函数内的操作包裹在 requestAnimationFrame 中:

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