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 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 设备每秒显示 60 帧,这为你和 UI 系统提供了大约 16.67 毫秒的时间来完成生成用户将在该时间间隔内在屏幕上看到的静态图片(帧)所需的所有工作。如果你无法在指定的 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 devices display 60 frames per second, which gives you and the UI system about 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 16.67ms, 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.

JS 帧率(JavaScript 线程)

¥JS frame rate (JavaScript thread)

对于大多数 React Native 应用,你的业务逻辑将在 JavaScript 线程上运行。这是你的 React 应用所在的地方,进行 API 调用,处理触摸事件等......对原生支持的视图的更新会在事件循环的每次迭代结束时、在帧截止日期之前分批发送到原生端(如果一切顺利)。如果 JavaScript 线程对某个帧没有响应,则该帧将被视为丢帧。例如,如果你要在复杂应用的根组件上调用 this.setState,并且导致重新渲染计算成本高昂的组件子树,则可以想象,这可能需要 200 毫秒并导致 12 帧被丢弃。任何由 JavaScript 控制的动画都会在这段时间内冻结。如果任何事情花费的时间超过 100 毫秒,用户就会感觉到。

¥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, etc... 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 call this.setState 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 anything takes longer than 100ms, the user will feel it.

这通常发生在 Navigator 转换期间:当你推送新路由时,JavaScript 线程需要渲染场景所需的所有组件,以便将正确的命令发送到原生端以创建支持视图。此处完成的工作通常会占用几帧并导致 jank,因为转换是由 JavaScript 线程控制的。有时组件会在 componentDidMount 上执行额外的工作,这可能会导致过渡中出现第二次卡顿。

¥This often happens during Navigator transitions: when you push a new route, the JavaScript thread needs to render all of the components necessary for the scene in order to send over the proper commands to the native side to create the backing views. It's common for the work being done here to take a few frames and cause jank because the transition is controlled by the JavaScript thread. Sometimes components will do additional work on componentDidMount, which might result in a second stutter in the transition.

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

¥Another 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)

很多人都注意到,NavigatorIOS 的开箱即用性能比 Navigator 更好。原因是过渡动画完全在主线程上完成,因此它们不会被 JavaScript 线程上的帧丢失中断。

¥Many people have noticed that performance of NavigatorIOS is better out of the box than Navigator. The reason for this is that the animations for the transitions are done entirely on the main thread, and 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 线程性能会受到很大影响。这是不可避免的:在运行时需要做更多的工作来为你提供良好的警告和错误消息,例如验证 propTypes 和各种其他断言。始终确保在 发布版本 中测试性能。

¥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, such as validating propTypes and various other assertions. 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:

{
"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.

ListView 初始渲染太慢或对于大型列表来说滚动性能较差

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

请改用新的 FlatListSectionList 组件。除了简化 API 之外,新的列表组件还具有显着的性能增强,其中最主要的一个是任意数量的行的内存使用量几乎恒定。

¥Use the new FlatList or SectionList component instead. Besides simplifying the API, the new list components also have significant performance enhancements, the main one being nearly constant memory usage for any number of rows.

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

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

重新渲染几乎没有变化的视图时,JS FPS 急剧下降

¥JS FPS plunges when re-rendering a view that hardly changes

如果你使用的是 ListView,则必须提供一个 rowHasChanged 函数,该函数可以通过快速确定某一行是否需要重新渲染来减少大量工作。如果你使用不可变数据结构,则这只需要进行引用相等性检查。

¥If you are using a ListView, you must provide a rowHasChanged function that can reduce a lot of work by quickly determining whether or not a row needs to be re-rendered. If you are using immutable data structures, this would only need to be a reference equality check.

同样,你可以实现 shouldComponentUpdate 并指明你希望组件重新渲染的确切条件。如果你编写纯组件(其中渲染函数的返回值完全依赖于 props 和 state),你可以利用 PureComponent 来为你完成此操作。再一次,不可变的数据结构对于保持速度非常有用 - 如果你必须对大量对象进行深入比较,那么重新渲染整个组件可能会更快,而且肯定需要更少的代码 。

¥Similarly, you can implement shouldComponentUpdate and indicate the exact conditions under which you would like the component to re-render. If you write pure components (where the return value of the render function is entirely dependent on props and state), you can leverage PureComponent to do this for you. Once again, immutable data structures are useful to keep this fast -- if you have to do a deep comparison of a large list of objects, it may be that re-rendering your entire component would be quicker, and it would certainly require less code.

由于同时在 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.

Animated API 目前在 JavaScript 线程上按需计算每个关键帧,除非你 设置 useNativeDriver: true,而 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 where I have used this is for 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

当你的图片顶部有带有透明背景的文本时,或者需要使用 Alpha 合成在每个帧上重新绘制视图的任何其他情况时,尤其如此。你会发现启用 shouldRasterizeIOSrenderToHardwareTextureAndroid 可以显着帮助解决此问题。

¥This is especially true 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 shouldRasterizeIOS or renderToHardwareTextureAndroid can help with this significantly.

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

¥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 上,每次调整图片组件的宽度或高度时,它都会从原始图片中重新裁剪和缩放。这可能非常昂贵,尤其是对于大图片。相反,请使用 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 执行 setState 导致大量工作并丢掉一些帧,则可能会发生这种情况。解决方案是将 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. If onPress does a setState that results in a lot of work and a few frames dropped, this may occur. A solution to this is to wrap any action inside of your onPress handler in requestAnimationFrame:

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

导航器转换缓慢

¥Slow navigator transitions

如上所述,Navigator 动画是由 JavaScript 线程控制的。想象一下 "从右推" 的场景转换:在每一帧中,新场景从右向左移动,从屏幕外开始(假设 x 偏移量为 320),并最终在场景位于 x 偏移量 0 时稳定下来。在此转换期间的每一帧,JavaScript 线程都需要向主线程发送新的 x 偏移量。如果 JavaScript 线程被锁定,它就无法执行此操作,因此该帧上不会发生更新,并且动画会断断续续。

¥As mentioned above, Navigator animations are controlled by the JavaScript thread. Imagine the "push from right" scene transition: each frame, the new scene is moved from the right to left, starting offscreen (let's say at an x-offset of 320) and ultimately settling when the scene sits at an x-offset of 0. Each frame during this transition, the JavaScript thread needs to send a new x-offset to the main thread. If the JavaScript thread is locked up, it cannot do this and so no update occurs on that frame and the animation stutters.

解决此问题的一种方法是允许将基于 JavaScript 的动画卸载到主线程。如果我们要使用这种方法执行与上面示例中相同的操作,我们可能会在开始转换时计算新场景的所有 x 偏移列表,并将它们发送到主线程以优化的方式执行 。现在 JavaScript 线程已经摆脱了这个责任,如果它在渲染场景时丢掉几帧也没什么大不了的 - 你可能甚至不会注意到,因为你会因为漂亮的过渡而分心。

¥One solution to this is to allow for JavaScript-based animations to be offloaded to the main thread. If we were to do the same thing as in the above example with this approach, we might calculate a list of all x-offsets for the new scene when we are starting the transition and send them to the main thread to execute in an optimized way. Now that the JavaScript thread is freed of this responsibility, it's not a big deal if it drops a few frames while rendering the scene -- you probably won't even notice because you will be too distracted by the pretty transition.

解决这个问题是新 React 导航 库背后的主要目标之一。React Navigation 中的视图使用原生组件和 Animated 库来提供在原生线程上运行的 60 FPS 动画。

¥Solving this is one of the main goals behind the new React Navigation library. The views in React Navigation use native components and the Animated library to deliver 60 FPS animations that are run on the native thread.