跳至主内容
版本:0.77

性能概述

非官方测试版翻译

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

选择 React Native 而非基于 WebView 的工具,一个重要原因是它能实现至少 60 帧/秒的流畅度,并为应用提供原生般的体验。我们致力于让 React Native 自动处理优化工作,使开发者能专注于应用逻辑而无需担忧性能问题。不过在某些领域,我们尚未完全实现这个目标;而在另一些场景中(如同直接编写原生代码),React Native 无法代您决定最佳优化方案。这时就需要手动介入。虽然我们默认提供如黄油般顺滑的 UI 性能,但某些情况下可能无法完全实现。

本指南将介绍基础知识,帮助您排查性能问题,并探讨常见性能问题的根源及解决方案

关于帧的核心知识

祖父母那代人将电影称为"活动图画"有其道理:视频中逼真的运动效果,其实是通过稳定速度快速切换静态图像制造的视觉幻象。我们将这些静态图像称为帧。每秒显示的帧数(FPS)直接影响视频(或用户界面)的流畅度与真实感。iOS 设备通常以 60 帧/秒运行,这意味着您和 UI 系统最多只有 16.67 毫秒来完成生成单帧图像的所有工作。若无法在此时间窗内完成帧渲染,就会发生"丢帧",导致 UI 响应迟滞。

现在让我们增加些复杂度:在应用中打开开发者菜单,启用Show Perf Monitor。您会注意到这里显示两种不同的帧率。

JS 帧率(JavaScript 线程)

多数 React Native 应用的业务逻辑都在 JavaScript 线程运行。这里承载着 React 应用的生命周期:API 调用、触摸事件处理等...对原生视图的更新会在事件循环每轮迭代结束时批处理发送到原生端(理想情况下在帧截止前完成)。若 JavaScript 线程在某帧期间无响应,即视为丢帧。例如,在复杂应用的根组件调用this.setState可能触发计算密集型子树的重新渲染,假设耗时 200 毫秒,将导致丢失 12 帧(200ms / 16.67ms ≈ 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 文件如下:

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

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

即使项目中没有直接调用 console.* 也建议使用该插件,因为第三方库可能包含此类调用。

ListView 初始渲染过慢或长列表滚动性能差

改用新的 FlatListSectionList 组件。除了简化 API,新列表组件还有显著的性能提升,主要特点是无论行数多少都能保持近乎恒定的内存占用。

如果 FlatList 渲染缓慢,请确保实现 getItemLayout 来跳过项目测量,从而优化渲染速度。

重新渲染几乎不变的视图时 JS FPS 骤降

使用 ListView 时必须提供 rowHasChanged 函数,通过快速判断行是否需要重新渲染来减少大量工作。如果使用不可变数据结构,只需进行引用相等性检查即可。

类似地,您可以实现 shouldComponentUpdate 来精确指定组件重新渲染的条件。如果是纯组件(渲染结果完全取决于 props 和 state),可以使用 PureComponent 自动处理。不可变数据结构同样有助于提升效率——如果需要对大型对象列表进行深度比较,直接重新渲染整个组件可能反而更快,且代码更简洁。

因 JavaScript 线程同时处理大量任务导致 FPS 下降

"导航转场卡顿"是最常见的表现,但其他场景也可能发生。使用 InteractionManager 是不错的解决方案,但如果延迟处理会影响动画期间的体验,可以考虑 LayoutAnimation。

当前 Animated API 默认在 JavaScript 线程按需计算每个关键帧(除非设置 useNativeDriver: true),而 LayoutAnimation 直接利用 Core Animation,不受 JS 线程和主线程掉帧影响。

我曾将此技术用于模态框动画(从顶部滑入并淡入半透明遮罩),同时初始化多个网络请求、渲染模态框内容并更新原视图。详见动画指南了解如何使用 LayoutAnimation。

注意事项:

  • LayoutAnimation 仅适用于触发即不管的动画(“静态”动画)——若需可中断动画,则必须使用 Animated

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

当透明背景文字覆盖在图片上方,或任何需要逐帧重绘视图并进行透明度合成的场景时尤为明显。启用 shouldRasterizeIOSrenderToHardwareTextureAndroid 能显著改善此问题。

请避免过度使用该属性,否则内存占用可能激增。使用这些属性时务必分析性能和内存占用。若视图不再需要移动,请及时关闭此属性。

图片尺寸动画导致 UI 线程 FPS 下降

在 iOS 上,每次调整 Image 组件的宽高都会触发原始图像的重新裁剪和缩放。这对大图尤其耗费资源。建议改用 transform: [{scale}] 样式属性实现尺寸动画(例如点击图片放大至全屏的场景)。

TouchableX 视图响应迟缓

若在响应触摸的同一帧内调整组件透明度或高亮状态,需等待 onPress 函数返回后才能看到效果。当 onPress 中的 setState 触发大量计算导致丢帧时,可将 onPress 处理函数中的操作包裹在 requestAnimationFrame 中解决:

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

导航器转场卡顿

如前所述,Navigator 动画由 JavaScript 线程控制。以“右侧推入”转场为例:新场景需从屏幕外(x偏移320)逐帧移动至x偏移0。若 JavaScript 线程阻塞导致偏移量更新失败,该帧动画即会卡顿。

解决方案是将 JavaScript 动画任务转移到主线程:在转场启动时预计算所有x偏移量,打包发送至主线程优化执行。JavaScript 线程因此解放,即使渲染时丢帧也不影响转场流畅度——精美动画足以转移用户注意力。

这正是新 React Navigation 库的核心目标:其视图采用原生组件结合 Animated 库,通过原生线程实现至少 60 FPS 的转场动画。