Skip to main content

性能概览

使用 React Native 而不是基于 WebView 的工具的一个令人信服的理由是实现每秒至少 60 帧并为你的应用提供原生外观和感觉。只要可行,我们的目标是让 React Native 自动处理优化,让你能够专注于你的应用,而不必担心性能。然而,在某些字段我们还没有完全达到这个水平,而在其他字段,React Native(类似于直接编写原生代码)无法确定最适合你的优化方法。在这种情况下,手动干预就变得必要了。我们努力在默认情况下提供极其流畅的 UI 性能,但在某些情况下可能无法做到这一点。

¥A compelling reason to use React Native instead of WebView-based tools is to achieve at least 60 frames per second and provide a native look and feel to your apps. Whenever feasible, we aim for React Native to handle optimizations automatically, allowing you to focus on your app without worrying about performance. However, there are certain areas where we haven't quite reached that level yet, and others where React Native (similar to writing native code directly) cannot determine the best optimization approach for you. In such cases, manual intervention becomes necessary. We strive to deliver buttery-smooth UI performance by default, but there may be instances where that isn't possible.

本指南旨在教你一些基础知识,以帮助你实现 解决性能问题,并讨论 问题的常见根源及其建议的解决方案

¥This guide is intended to teach you some basics to help you to troubleshoot performance issues, as well as discuss common sources of problems and their suggested solutions.

关于框架你需要了解的知识

¥What you need to know about frames

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

¥Your grandparents' generation called movies "moving pictures" for a reason: realistic motion in video is an illusion created by quickly changing static images at a consistent speed. We refer to each of these images as frames. The number of frames that is displayed each second has a direct impact on how smooth and ultimately life-like a video (or user interface) seems to be. iOS and Android devices display at least 60 frames per second, which gives you and the UI system at most 16.67ms to do all of the work needed to generate the static image (frame) that the user will see on the screen for that interval. If you are unable to do the work necessary to generate that frame within the allotted time slot, then you will "drop a frame" and the UI will appear unresponsive.

现在让事情变得有点混乱,在你的应用中打开 开发菜单 并切换 Show Perf Monitor。你会注意到有两种不同的帧速率。

¥Now to confuse the matter a little bit, open up the Dev Menu in your app and toggle Show Perf Monitor. You will notice that there are two different frame rates.

Performance Monitor screenshot

JS 帧率(JavaScript 线程)

¥JS frame rate (JavaScript thread)

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

¥For most React Native applications, your business logic will run on the JavaScript thread. This is where your React application lives, API calls are made, touch events are processed, and more. Updates to native-backed views are batched and sent over to the native side at the end of each iteration of the event loop, before the frame deadline (if all goes well). If the JavaScript thread is unresponsive for a frame, it will be considered a dropped frame. For example, if you were to set a new state on the root component of a complex application and it resulted in re-rendering computationally expensive component subtrees, it's conceivable that this might take 200ms and result in 12 frames being dropped. Any animations controlled by JavaScript would appear to freeze during that time. If enough frames are dropped, the user will feel it.

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

¥An example is responding to touches: if you are doing work across multiple frames on the JavaScript thread, you might notice a delay in responding to TouchableOpacity, for example. This is because the JavaScript thread is busy and cannot process the raw touch events sent over from the main thread. As a result, TouchableOpacity cannot react to the touch events and command the native view to adjust its opacity.

UI 帧率(主线程)

¥UI frame rate (main thread)

你可能已经注意到,原生堆栈导航器(例如 React Navigation 提供的 @react-navigation/native-stack)的性能比基于 JavaScript 的堆栈导航器更好。这是因为过渡动画是在原生主 UI 线程上执行的,因此它们不会被 JavaScript 线程上的丢帧打断。

¥You may have noticed that performance of native stack navigators (such as the @react-navigation/native-stack provided by React Navigation) is better out of the box than JavaScript-based stack navigators. This is because the transition animations are executed on the native main UI thread, so they are not interrupted by frame drops on the JavaScript thread.

同样,当 JavaScript 线程被锁定时,你可以愉快地在 ScrollView 上上下滚动,因为 ScrollView 位于主线程上。滚动事件被分派到 JS 线程,但滚动事件的发生并不需要接收它们。

¥Similarly, you can happily scroll up and down through a ScrollView when the JavaScript thread is locked up because the ScrollView lives on the main thread. The scroll events are dispatched to the JS thread, but their receipt is not necessary for the scroll to occur.

性能问题的常见来源

¥Common sources of performance problems

在开发模式下运行 (dev=true)

¥Running in development mode (dev=true)

在开发模式下运行时,JavaScript 线程性能会受到很大影响。这是不可避免的:运行时需要做很多工作才能为你提供良好的警告和错误消息。始终确保在 发布版本 中测试性能。

¥JavaScript thread performance suffers greatly when running in dev mode. This is unavoidable: a lot more work needs to be done at runtime to provide you with good warnings and error messages. Always make sure to test performance in release builds.

使用 console.log 语句

¥Using console.log statements

运行打包应用时,这些语句可能会导致 JavaScript 线程出现大瓶颈。这包括来自调试库(例如 redux-logger)的调用,因此请确保在打包之前删除它们。你还可以使用此 巴贝尔插件 来删除所有 console.* 调用。你需要先使用 npm i babel-plugin-transform-remove-console --save-dev 安装它,然后编辑项目目录下的 .babelrc 文件,如下所示:

¥When running a bundled app, these statements can cause a big bottleneck in the JavaScript thread. This includes calls from debugging libraries such as redux-logger, so make sure to remove them before bundling. You can also use this babel plugin that removes all the console.* calls. You need to install it first with npm i babel-plugin-transform-remove-console --save-dev, and then edit the .babelrc file under your project directory like this:

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

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

¥This will automatically remove all console.* calls in the release (production) versions of your project.

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

¥It is recommended to use the plugin even if no console.* calls are made in your project. A third party library could also call them.

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

¥FlatList rendering is too slow or scroll performance is bad for large lists

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

¥If your FlatList is rendering slowly, be sure that you've implemented getItemLayout to optimize rendering speed by skipping measurement of the rendered items.

还有其他针对性能进行了优化的第三方列表库,包括 FlashList图例列表

¥There are also other third-party list libraries that are optimized for performance, including FlashList and Legend List.

由于同时在 JavaScript 线程上执行大量工作,导致 JS 线程 FPS 下降

¥Dropping JS thread FPS because of doing a lot of work on the JavaScript thread at the same time

"导航器转换缓慢" 是最常见的表现形式,但有时也会发生这种情况。使用 InteractionManager 可能是一个不错的方法,但如果用户体验成本过高,以至于无法在动画期间延迟工作,那么你可能需要考虑使用 LayoutAnimation

¥"Slow Navigator transitions" is the most common manifestation of this, but there are other times this can happen. Using InteractionManager can be a good approach, but if the user experience cost is too high to delay work during an animation, then you might want to consider LayoutAnimation.

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

¥The Animated API currently calculates each keyframe on-demand on the JavaScript thread unless you set useNativeDriver: true, while LayoutAnimation leverages Core Animation and is unaffected by JS thread and main thread frame drops.

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

¥One case for using this is animating in a modal (sliding down from top and fading in a translucent overlay) while initializing and perhaps receiving responses for several network requests, rendering the contents of the modal, and updating the view where the modal was opened from. See the Animations guide for more information about how to use LayoutAnimation.

注意事项:

¥Caveats:

  • LayoutAnimation 仅适用于“触发后不管”动画("static" 动画) - 如果必须可中断,则需要使用 Animated

    ¥LayoutAnimation only works for fire-and-forget animations ("static" animations) -- if it must be interruptible, you will need to use Animated.

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

¥Moving a view on the screen (scrolling, translating, rotating) drops UI thread FPS

在 Android 上尤其如此,当你将具有透明背景的文本放置在图片上方时,或者在任何其他需要 Alpha 合成来重新绘制图片的情况下查看每一帧。你会发现启用 renderToHardwareTextureAndroid 可以显著改善这一点。对于 iOS,shouldRasterizeIOS 已默认启用。

¥This is especially true on Android when you have text with a transparent background positioned on top of an image, or any other situation where alpha compositing would be required to re-draw the view on each frame. You will find that enabling renderToHardwareTextureAndroid can help with this significantly. For iOS, shouldRasterizeIOS is already enabled by default.

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

¥Be careful not to overuse this or your memory usage could go through the roof. Profile your performance and memory usage when using these props. If you don't plan to move a view anymore, turn this property off.

对图片大小进行动画处理会降低 UI 线程 FPS

¥Animating the size of an image drops UI thread FPS

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

¥On iOS, each time you adjust the width or height of an Image component it is re-cropped and scaled from the original image. This can be very expensive, especially for large images. Instead, use the transform: [{scale}] style property to animate the size. An example of when you might do this is when you tap an image and zoom it in to full screen.

我的 TouchableX 视图响应不太灵敏

¥My TouchableX view isn't very responsive

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

¥Sometimes, if we do an action in the same frame that we are adjusting the opacity or highlight of a component that is responding to a touch, we won't see that effect until after the onPress function has returned. This may occur if onPress sets a state that results in a heavy re-render and a few frames are dropped as a result. A solution to this is to wrap any action inside of your onPress handler in requestAnimationFrame:

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