首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Performance

使用React Native代替基于WebView的工具的一个令人信服的理由是每秒达到60帧,并为您的应用程序提供原生的外观和感觉。在可能的情况下,我们希望React Native做正确的事情,并帮助您专注于应用程序而不是性能优化,但有些领域我们还没有完全实现,还有一些领域则是React Native(类似于编写本机直接编码)无法确定为您优化的最佳方式,因此需要手动干预。我们尽力默认提供顺滑的UI性能,但有时这是不可能的。

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

你需要了解框架

你的祖父母一代称电影为“动态图片”,原因是:视频中的逼真运动是以一致的速度快速改变静态图像而产生的错觉。我们将每个图像称为帧。每秒显示的帧数直接影响视频(或用户界面)的流畅程度和最终效果。iOS设备每秒显示60帧,这为您和UI系统提供了大约16.67ms的时间来完成生成静态图像(帧)所需的所有工作,用户将在屏幕上看到该静态图像(帧)。如果您无法在分配的16.67毫秒内完成生成该帧所需的工作,那么您将“丢帧”,并且UI将显示无响应。

现在将这个问题混淆一下,打开应用程序中的开发者菜单并切换Show Perf Monitor。您会注意到有两种不同的帧速率。

JS frame rate (JavaScript thread)

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

这经常发生在Navigator转换过程中:当您推送新路由时,JavaScript线程需要呈现场景所需的所有组件,以便将适当的命令发送到本机端以创建支持视图。在这里完成的工作通常会花费几个帧并导致Jank,因为转换由JavaScript线程控制。有时候组件会做额外的工作componentDidMount,这可能会导致转换中出现第二个问题。

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

UI frame rate (main thread)

许多人已经注意到,表现NavigatorIOS比盒子更好Navigator。原因是转换的动画完全在主线程上完成,因此它们不会被JavaScript线程中的帧丢弃中断。

同样,ScrollView当JavaScript线程因为ScrollView主线程中的生命而被锁定时,您可以愉快地向上和向下滚动。滚动事件被分派给JS线程,但他们的收据不需要滚动发生。

Common sources of performance problems

Running in development mode (dev=true)

在开发模式下运行时,JavaScript线程性能会受到很大影响。这是不可避免的:在运行时需要做更多的工作来为您提供良好的警告和错误消息,例如验证propTypes和其他各种断言。始终确保在发布版本中测试性能。

Using console.log statements

运行捆绑应用程序时,这些语句可能会在JavaScript线程中造成严重瓶颈。这包括调试库(如redux-logger)的调用,因此请确保在捆绑之前将其删除。你也可以使用这个babel插件来删除所有的console.*电话。您需要先安装它npm i babel-plugin-transform-remove-console --save,然后.babelrc在您的项目目录下编辑文件,如下所示:

代码语言:javascript
复制
{
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

这将自动删除console.*项目的发行(生产)版本中的所有呼叫。

ListView 初始渲染速度太慢或滚动性能对大型列表不利

改用新的FlatListSectionList组件。除了简化API之外,新的列表组件还具有显着的性能增强,主要的是对于任意数量的行几乎不变的内存使用。

如果您FlatList的渲染速度较慢,请确保您已实施getItemLayout,通过跳过对渲染项目的测量来优化渲染速度。

当重新渲染一个几乎没有变化的视图时,JS FPS陷入沉没

如果您使用的是ListView,则必须提供一个rowHasChanged函数,通过快速确定是否需要重新渲染行来减少大量工作。如果您使用不可变的数据结构,这将与参考相等性检查一样简单。

同样,您可以实现shouldComponentUpdate并指出您希望组件重新呈现的确切条件。如果您编写纯组件(渲染函数的返回值完全依赖于道具和状态),则可以利用PureRenderMixin为您执行此操作。再一次,不可变的数据结构对于保持这种快速性非常有用 - 如果您必须对大量对象进行深度比较,则可能会更快地渲染整个组件,并且它肯定会需要更少的代码。

由于同时在JavaScript线程上做了大量工作,因此删除了JS线程FPS

“慢导航转换”是这种情况最常见的表现,但还有其他时候会发生这种情况。使用InteractionManager可能是一种好方法,但如果用户体验成本太高而无法在动画期间延迟工作,那么您可能需要考虑LayoutAnimation。

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

我已经使用过的一种情况是,在模式中进行动画处理(从顶部滑下并在半透明叠加层中淡出),同时初始化并可能接收多个网络请求的响应,渲染模态的内容,并更新模态被打开了。有关如何使用LayoutAnimation的更多信息,请参阅动画指南。

注意事项:

  • LayoutAnimation only works for fire-and-forget animations ("static" animations) -- if it must be interruptible, you will need to use Animated.Moving a view on the screen (scrolling, translating, rotating) drops UI thread FPSThis 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.Animating the size of an image drops UI thread FPSOn 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.My TouchableX view isn't very responsiveSometimes, 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() { // Always use TimerMixin with requestAnimationFrame, setTimeout and // setInterval this.requestAnimationFrame(() => { this.doExpensiveAction(); }); }Slow navigator transitionsAs 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.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.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.ProfilingUse the built-in profiler to get detailed information about work done in the JavaScript thread and main thread side-by-side. Access it by selecting Perf Monitor from the Debug menu.For iOS, Instruments is an invaluable tool, and on Android you should learn to use systrace.You can also use react-addons-perf to get insights into where React is spending time when rendering your components.Another way to profile JavaScript is to use the Chrome profiler while debugging. This won't give you accurate results as the code is running in Chrome but will give you a general idea of where bottlenecks might be.But first, make sure that Development Mode is OFF! You should see __DEV__ === false, development-level warning are OFF, performance optimizations are ON in your application logs.Profiling Android UI Performance with systraceAndroid supports 10k+ different phones and is generalized to support software rendering: the framework architecture and need to generalize across many hardware targets unfortunately means you get less for free relative to iOS. But sometimes, there are things you can improve -- and many times it's not native code's fault at all!The first step for debugging this jank is to answer the fundamental question of where your time is being spent during each 16ms frame. For that, we'll be using a standard Android profiling tool called systrace.systrace is a standard Android marker-based profiling tool (and is installed when you install the Android platform-tools package). Profiled code blocks are surrounded by start/end markers which are then visualized in a colorful chart format. Both the Android SDK and React Native framework provide standard markers that you can visualize.1. Collecting a traceFirst, connect a device that exhibits the stuttering you want to investigate to your computer via USB and get it to the point right before the navigation/animation you want to profile. Run systrace as follows:$ <path_to_android_sdk>/platform-tools/systrace/systrace.py --time=10 -o trace.html sched gfx view -a <your_package_name>A quick breakdown of this command:
  • time 是以秒为单位收集曲线的时间长度
  • schedgfx并且view是我们关心的android SDK标记(标记集合):sched为您提供有关手机每个核心运行的信息,gfx为您提供图形信息,例如框架边界,并view为您提供有关度量,布局和绘制的信息通行证
  • -a <your_package_name>启用特定于应用程序的标记,特别是内置于React Native框架中的标记。your_package_name可以在AndroidManifest.xml您的应用程序中找到并且看起来像com.example.app

一旦跟踪开始收集,执行您关心的动画或交互。在跟踪结束时,systrace会为您提供指向您可以在浏览器中打开的跟踪的链接。

2.读踪迹

在浏览器(最好是Chrome)中打开跟踪之后,您应该看到如下所示的内容:

提示:使用WASD键进行扫描和缩放

如果您的跟踪.html文件无法正确打开,请检查您的浏览器控制台以查找以下内容:

由于Object.observe在最近的浏览器中已被弃用,您可能必须从Google Chrome追踪工具中打开该文件。你可以这样做:

  • 在chrome chrome中打开标签://跟踪
  • 选择加载
  • 选择从前一个命令生成的html文件。

启用垂直同步突出显示 选中屏幕右上角的此复选框以突出显示16ms帧边界:

你应该看到上面截图中的斑马条纹。如果不这样做,请尝试分析其他设备:三星已知存在显示vsyncs的问题,而Nexus系列通常非常可靠。

3.找到你的过程

滚动,直到看到(部分)包装名称。在这种情况下,我正在进行性能分析com.facebook.adsmanager,这是book.adsmanager因为内核中的线程名称限制很愚蠢。

在左侧,您会看到一组与右侧时间线行相对应的线程。有几个线程我们关心我们的目的:UI线程(其中包含您的包名称或名称UI线程)mqt_js,和mqt_native_modules。如果您使用Android 5+,我们也关心渲染线程。

  • UI线程。这是标准的android度量/布局/绘制发生的地方。右侧的线程名称将成为您的包名称(在我的案例中为book.adsmanager)或UI线程。您在这个线程看到的事件应该是这个样子,并有做Choreographertraversals以及DispatchUI
  • JS线程。这是执行JavaScript的地方。线程名称将是mqt_js或者<...>取决于设备上内核的协作方式。要确定它,如果它没有名字,认准的事一样JSCallBridge.executeJSCall等:
  • 原生模块线程。这是UIManager执行本地模块调用(例如,)的地方。线程名称将是mqt_native_modules<...>。要确定它在后一种情况下,认准的事像NativeCallcallJavaModuleMethod以及onBatchComplete
  • 奖金:渲染线程。如果您使用的是Android L(5.0)或更高版本,则您的应用程序中也会有一个渲染线程。该线程生成用于绘制UI的实际OpenGL命令。线程名称将是RenderThread<...>。要在后一种情况下识别它,请查找以下内容DrawFramequeueBuffer

识别罪魁祸首

平滑的动画应该如下所示:

每种颜色变化都是一个框架 - 请记住,为了显示一个框架,我们所有的UI工作都需要在该16ms周期结束时完成。请注意,没有线程靠近帧边界。像这样呈现的应用程序呈现为60 FPS。

但是,如果你注意到了印章,你可能会看到类似这样的内容:

注意,JS线程基本上始终在跨越帧边界执行!这个应用程序不是以60 FPS渲染。在这种情况下,问题在于JS

您也可能会看到类似这样的内容:

在这种情况下,UI和渲染线程是跨越框架边界的工作线程。我们试图在每一帧上渲染的用户界面需要完成很多工作。在这种情况下,问题在于呈现的本地视图

在这一点上,你会有一些非常有用的信息来通知你的下一步。

解决JavaScript问题

如果你发现了一个JS问题,在你正在执行的特定JS中寻找线索。在上面的场景中,我们看到RCTEventEmitter每帧被称为多次。这是上面跟踪的JS线程的放大:

这看起来不正确。为什么这么频繁地被调用?他们真的是不同的事件吗?这些问题的答案可能取决于您的产品代码。很多时候,你会想看看shouldComponentUpdate

解决原生UI问题

如果您确定了本机用户界面问题,通常有两种情况:

  • 您试图绘制每个框架的用户界面涉及到GPU上的太多工作,或者
  • 您在动画/交互期间构建新的用户界面(例如,在滚动期间加载新内容)。
GPU工作太多

在第一种情况下,您将看到具有UI线程和/或渲染线程的跟踪,如下所示:

注意花在DrawFrame这个跨越帧边界上的时间很长。这是等待GPU从前一帧消耗其命令缓冲区的时间。

为了缓解这种情况,您应该:

  • 调查使用renderToHardwareTextureAndroid正在动画/转换的复杂静态内容(例如Navigator幻灯片/ alpha动画)
  • 请确保您没有使用needsOffscreenAlphaCompositing,默认情况下它是禁用的,因为它在大多数情况下会大大增加GPU上的每帧负载。

如果这些都无济于事,并且您想更深入地了解GPU究竟在做什么,那么您可以查看Tracer for OpenGL ES

在UI线程上创建新的视图

在第二种情况下,您会看到更像这样的内容:

注意,首先JS线程想了一下,然后你看到在本地模块线程上完成了一些工作,然后在UI线程上进行了昂贵的遍历。

除非您能够在交互之后推迟创建新的用户界面,或者您可以简化您创建的用户界面,否则没有简单的方法可以缓解这种情况。原生反应团队正在为此设计一个基础架构级别的解决方案,这将允许在主线程中创建和配置新UI,从而使交互能够顺利进行。

扫码关注腾讯云开发者

领取腾讯云代金券