响应式 性能
In mobile apps, performance is critical for a good user experience. At Tableau we have been using React Native for three years now, and we have learned a lot about getting good native-like performance with it. For the most part it doesn’t take much effort, as long as you have an understanding for how things work, follow some good patterns, and avoid some pitfalls. In this article we share the most important things we have learned.
在移动应用程序中,性能对于良好的用户体验至关重要。 在Tableau中,我们已经使用React Native三年了,并且我们已经学到了很多有关获得类似Native的良好性能的知识。 在大多数情况下,只要您对事物的工作方式有所了解,遵循一些良好的模式并避免一些陷阱,就无需花费太多精力。 在本文中,我们分享了我们学到的最重要的东西。
了解React Native Performance基础 (Understand React Native Performance Basics)
React Native maintains a good page on performance with a wealth of information. Make sure you are somewhat familiar with all of it, though not everything applies to every project (we have never had to enable RAM bundles or use inline requires for example).
React Native通过大量信息在性能上保持良好的页面 。 确保您对所有内容都有些熟悉,尽管并非所有内容都适用于每个项目(例如,我们从不必启用RAM捆绑包或使用内联要求)。
A couple things that are mentioned on that page are worth emphasizing here. First performance problems should be checked on a real device with a release build. Especially on Android. The difference between the Android emulator and a real device, and between a debug and release build, can be enormous. Secondly, if you are doing any kind of animation, make sure to enable useNativeDriver in the animation. It can make a huge difference.
该页面上提到的几件事在这里值得强调。 应该在具有发布版本的真实设备上检查首次性能问题。 特别是在Android上。 Android模拟器和真实设备之间以及调试和发行版本之间的差异可能很大。 其次,如果您正在执行任何类型的动画,请确保在动画中启用useNativeDriver 。 它可以产生巨大的变化。
了解对帐 (Understand Reconciliation)
The most common performance problem we have had is when the JavaScript side of the app is bogged down while the native side isn’t. This means that native UI elements are responsive (scrolling is fine, tap animations are fine, etc) but the app otherwise feels unresponsive or laggy. This is because the JavaScript thread is too busy processing something and not responding to user interactions.
我们遇到的最常见的性能问题是应用程序JavaScript端陷入困境,而本机端则没有陷入困境。 这意味着本机UI元素可以响应(滚动很好,点击动画很好,等等),但是应用程序否则感觉没有响应或太慢。 这是因为JavaScript线程太忙于处理某些内容,而没有响应用户交互。
When we have hit this it has always been related to rendering some expensive React component, or a ton of simple React components, too often and unnecessarily.
当我们遇到这个问题时,它总是与过于频繁和不必要地渲染一些昂贵的React组件或大量简单的React组件有关。
React also has a good page about performance that is worth absorbing. A lot of the page isn’t all that relevant to React Native, but the parts on Avoiding Reconciliation and the React Profiler are (more on the profiler below).
React也有一个关于性能的好页面 ,值得吸收。 页面上的很多内容与React Native无关,但是关于避免对帐和React Profiler的部分是(下面的Profiler中有更多内容)。
Briefly, reconciliation is the process React uses to decide how to update the underlying DOM (React web) or native UI elements (React Native) based on what is rendered by your JavaScript components. Read the React documentation on reconciliation for more details.
简而言之,协调是React用于根据JavaScript组件呈现的内容来决定如何更新基础DOM(React web)或本机UI元素(React Native)的过程。 阅读关于对帐的React 文档以获取更多详细信息。
In React Native we have found that we really benefit from avoiding reconciliation, more-so than in web apps. Avoiding reconciliation translates to the JavaScript thread spending much less time on rendering, freeing it up to be responsive to user interactions.
与Web应用程序相比,在React Native中,我们发现避免对帐确实受益匪浅。 避免和解可以转化为JavaScript线程在渲染上花费更少的时间,从而将其释放出来以响应用户交互。
使用PureComponent(或React.memo) (Use PureComponent (or React.memo))
To avoid unnecessary rendering and reconciliation we made it a rule that all React components we write should derive from PureComponent
. We currently only use class-style components, so if you are writing functional components and using hooks you can use React.memo to get the same behavior.
为了避免不必要的渲染和协调,我们制定了一条规则,即我们编写的所有React组件都应源自PureComponent
。 当前,我们仅使用类样式的组件,因此,如果您正在编写功能组件并使用钩子,则可以使用React.memo来获得相同的行为。
All PureComponent
does is prevent rendering in cases where the props or state haven't changed. It detects changes via a simple shallow equality comparison: iterating over all the keys of the old and new props or state objects, and then doing a strict equality comparison on each pair of values. If there is a change, the component is re-rendered, but if there is no change it won’t be re-rendered and (importantly) neither will all its children.
在PureComponent
或状态未更改的情况下, PureComponent
所做的所有PureComponent
都是防止渲染。 它通过简单的浅层相等性比较来检测更改:迭代新旧道具或状态对象的所有键,然后对每对值进行严格的相等性比较。 如果有更改,将重新渲染该组件,但是如果没有更改,则将不会重新渲染,并且(重要的是)所有子组件也不会被渲染。
You will find advice out there suggesting that you should be more nuanced in your use of PureComponent
, but as a blanket rule it has worked out well for us at Tableau. Aside from reducing the overall amount of rendering that happens it also has the nice effect of making it easier to tell why a component is being rendered too often, since you know it should only be doing so because of changes to its props or state.
您会发现一些建议,建议您在使用PureComponent
,但作为一个总括规则,Tableau对我们来说效果很好。 除了减少发生的渲染总量之外,它还具有使您更容易分辨为什么过于频繁渲染组件的良好效果,因为您知道它仅应由于其道具或状态的更改而这样做。
了解react-redux的工作原理 (Understand how react-redux works)
Redux is a core part of our architecture in the Tableau mobile app, and if the same is true for your app you should understand how changes to your Redux state tree cause React components to update. We haven’t started using hooks yet so we are going to primarily be talking about mapStateToProps
, but most of what we say should apply to the newer useSelector
hook (though it is not exactly the same, as noted below).
Redux是Tableau移动应用程序中我们体系结构的核心部分,如果您的应用程序也是如此,则您应该了解Redux状态树的更改如何导致React组件更新。 我们还没有开始使用钩子,因此我们主要要讨论mapStateToProps
,但是我们所说的大多数内容都应适用于较新的useSelector
钩子(尽管如下所述,它并不完全相同)。
The most important thing to understand is that your mapStateToProps
(or useSelector
) functions will be run every time a Redux action is dispatched.
要了解的最重要的事情是, 每次调度Redux动作时,都会运行mapStateToProps
(或useSelector
)函数。
That means that presentational components (in redux terminology) which get their props from mapStateToProps
(or useSelector
) may be re-rendered every time any action is dispatched.
这意味着从mapStateToProps
(或useSelector
)获取其道具的表示性组件( 用redux术语 useSelector
)可以在每次调度任何动作时重新渲染。
Now react-redux will try to avoid rendering the presentational components by comparing the new result of mapStateToProps
with the prior result, doing a shallow equality comparison similar to how PureComponent
works, but it is easy to break this optimization and cause lots of unnecessary rendering if you aren’t careful about what you return.
现在的React,终极版会尽量避免通过的新的结果比较渲染表象的成分mapStateToProps
现有结果,做类似如何浅相等比较PureComponent
作品,但它很容易打破这种优化,原因很多不必要的渲染如果您对返回的内容并不小心。
Note thatuseSelector
does not do a shallow comparison on objects, rather just a simple ===
comparison, so you should definitely read up on how to use it.
请注意, useSelector
不会对对象进行浅层比较,而只是简单的===
比较,因此您绝对应该阅读如何使用它 。
避免在渲染过程中以及在mapStateToProps中创建新的对象,数组或函数 (Avoid creating new objects, arrays, or functions during rendering and in mapStateToProps)
It is easy to accidentally break the shallow equality checks that both PureComponent
and mapStateToProps
use to avoid unnecessary rendering by always creating new objects, arrays, or functions.
通过始终创建新的对象,数组或函数,很容易意外破坏PureComponent
和mapStateToProps
使用的浅层相等性检查,以避免不必要的呈现。
A simple silly example:
一个简单的愚蠢的例子:
In this example each of the three props getting passed to MyPureComponent
would, by themselves, break PureComponent
. Meaning that whenever the parent component re-renders, MyPureComponent
will also re-render.
在此示例中,传递给MyPureComponent
的三个道具中的每一个都会自行破坏PureComponent
。 这意味着每当父组件重新渲染时, MyPureComponent
也会重新渲染。
This is because of how the shallow equality checking works. For objects, arrays, and functions it is checking whether the new prop is the same instance as the old prop. In the example above, the props are always new instances so the equality check with the old props will always fail.
这是因为浅层相等性检查是如何工作的。 对于对象,数组和功能是检查新的道具是否是相同的实例如旧道具。 在上面的示例中,道具始终是新实例,因此与旧道具的相等性检查将始终失败。
Not avoiding this is quite a bit more risky in the mapStateToProps
case than in the PureComponent
case. Often, a component isn't individually expensive to render so you may not ever notice a problem, but because mapStateToProps
gets executed so often it can trigger lots of unnecessary rendering.
在mapStateToProps
情况下,不避免这种情况比在PureComponent
情况下的风险要PureComponent
。 通常,组件的渲染成本并不高,因此您可能永远不会注意到问题,但是由于mapStateToProps
经常执行,因此它可能触发许多不必要的渲染。
In some cases, you can solve this by turning the objects, arrays, and functions into constants or otherwise pulling them out of the function so they aren’t being recreated all the time. You can also avoid passing objects as props. For example instead of passing an object with two properties as a single prop, just create two different props.
在某些情况下,您可以通过将对象,数组和函数转换为常量或以其他方式将它们从函数中拉出来解决这些问题,从而避免一直在重新创建它们。 您还可以避免将对象作为道具传递。 例如,不要将具有两个属性的对象作为单个道具传递,而只需创建两个不同的道具。
Memoization is a good solution too, since memoized functions will compute the result (i.e. the array or object) once and cache it. Subsequent calls to the memoized function will just return the cached result. That means it will be the same instance as whatever was previously returned, and thus it won’t fail a shallow equality check. Consider using lodash’s memoize function, reselect, re-reselect, as well as React’s useMemo if you use hooks.
记忆化也是一个很好的解决方案,因为记忆化的函数将一次计算结果(即数组或对象)并将其缓存。 随后调用备忘录功能将仅返回缓存的结果。 这意味着它将与先前返回的实例处于同一实例,因此它不会通过浅层相等性检查。 考虑使用lodash的memoize的功能, 重新选择 , 再重新选择 ,以及作出React的useMemo如果使用挂钩。
避免在渲染时以及在mapStateToProps中重复执行昂贵的计算 (Avoid doing repeated expensive computations when rendering and in mapStateToProps)
This may be obvious but you don’t want to be doing anything remotely expensive repeatedly during rendering, and similarly in mapStateToProps
since that gets run on every redux action. There are a variety of ways to avoid this. If you have a class component you can move the computation to your constructor
(or componentDidMount
), though make sure you recompute it if you need to when the props change. You could also do the computation asynchronously by using the React Native InteractionManager’s runAfterInteraction
function or the standard setTimeout
function and storing the result in the state. You can also use memoization.
这可能很明显,但是您不希望在渲染期间重复进行任何昂贵的远程操作,并且类似地在mapStateToProps
因为该操作会在每个redux操作上运行。 有多种方法可以避免这种情况。 如果您有一个类组件,则可以将计算移至constructor
(或componentDidMount
),但是如果需要更改道具,请确保重新计算。 您还可以通过使用React Native InteractionManager的runAfterInteraction
函数或标准setTimeout
函数并将结果存储在状态中进行异步计算。 您也可以使用备忘录。
了解缓存在已记录功能中的工作方式 (Understand how caching works in memoized functions)
To get the benefit of memoized functions you have to understand how they cache values. You can accidentally cause a memoized function to recompute the cached value every time it is called, which defeats the whole purpose of memoization. The behavior varies by library, but we’ll cover a few problems we have hit here.
为了获得记忆功能的好处,您必须了解它们如何缓存值。 您可能会意外地使一个被记忆的函数在每次被调用时重新计算缓存的值,这违背了整个记忆的目的。 行为因库而异,但我们将介绍这里遇到的一些问题。
Selectors created by reselect have a cache size of one. This means that whenever they are called with different arguments they will recompute the result. So if a selector is being called often with different arguments it will not be effectively using its cache and will be creating new objects all the time. re-reselect was created to address this problem.
通过重选创建的选择器的缓存大小为1。 这意味着无论何时使用不同的参数调用它们,它们都将重新计算结果。 因此,如果经常使用不同的参数调用选择器,则选择器将无法有效使用其缓存,并且会一直创建新对象。 创建了重新选择以解决此问题。
Also for both reselect and re-reselect you will want to make sure that the functions that you pass to createSelector
or createCachedSelector
before the computation function (e.g. F1
to FN-1
in createSelector(F1, F2, ..., FN-1, FN)
, FN
being the computation function) narrow down the inputs to the computation function to what it really needs.
另外两个重选和再重新选择,你会希望确保该功能,你传递给createSelector
或createCachedSelector
运算功能之前 (例如F1
到FN-1
在createSelector(F1, F2, ..., FN-1, FN)
( FN
是计算功能)将计算功能的输入范围缩小到实际需要的范围。
For a contrived example, compare these two selectors created using reselect:
对于一个人为的示例,请比较使用reselect创建的这两个选择器:
These two selectors are very similar, and compute the same result, but the second function will only recompute its result when stateTree.allFoos
changes (specifically when it is a new array). The first function will recompute its result whenever stateTree
changes, producing a new array. This will mean that any PureComponent
relying on the result of getAllFooNames1
will re-render a lot more often than you'd expect.
这两个选择器非常相似,并且计算出相同的结果,但是第二个函数仅在stateTree.allFoos
发生更改时(特别是在它是一个新数组时)才重新计算其结果。 每当stateTree
更改时,第一个函数将重新计算其结果,从而生成一个新数组。 这将意味着任何PureComponent
依托的结果getAllFooNames1
会往往比你期望的重新渲染了很多。
If you are using lodash’s memoize
function, make sure you aren't calling memoize
itself in during rendering or in mapStateToProps
. Every time you call memoize
it returns a new function with a new (empty) cache. So you will want to store that created function somewhere.
如果您正在使用lodash的memoize
功能,确保您不会调用memoize
渲染过程中或本身就是mapStateToProps
。 每次调用memoize
它都会返回具有新(空)缓存的新功能。 因此,您将需要将创建的函数存储在某个地方。
避免不必要地使道具穿过多层组件 (Avoid passing props unnecessarily through multiple layers of components)
This can also break PureComponent
. You might have a component high up in your component tree that sets a prop (e.g. "isRefreshing"
) and passes that down to its child components. The child components don't use it, but pass the prop on down to their own children. Repeat that a few times, and finally you get to the components deep in the tree that care about the prop.
这也会破坏PureComponent
。 您可能在组件树中有一个较高的组件,该组件设置了一个"isRefreshing"
(例如"isRefreshing"
)并将其传递给其子组件。 子组件不使用它,而是将道具传递给自己的子组件。 重复几次,最后进入树中深处关心道具的组件。
The issue is that when the prop does change all the components in the tree have to be re-rendered, even though most of them don’t even use the prop.
问题是,当prop确实发生更改时,树中的所有组件都必须重新渲染,即使其中大多数甚至都不使用prop。
You can fix this a couple ways. If that prop is coming from redux (via a mapStateToProps
function), consider just turning the components that need the prop into container components so they can retrieve it from redux themselves.
您可以通过几种方法解决此问题。 如果该道具来自redux(通过mapStateToProps
函数),请考虑将需要道具的组件转换为容器组件,以便他们可以从redux本身检索它。
A different solution would be to use the React context API. The parent component can create a context and put the prop in there. Then the components that care about the prop can just use the context to get the value.
另一种解决方案是使用React上下文API 。 父组件可以创建上下文并将prop放在其中。 这样,关心道具的组件就可以使用上下文来获取值。
使用内置的探查器和React探查器 (Use the built-in profiler and the React profiler)
Even if you follow all sorts of good practices you can still end up with a performance problem. When you do need to dive deeper, there are some good tools to use.
即使遵循各种良好实践,您仍然可能会遇到性能问题。 当您确实需要更深入地研究时,可以使用一些好的工具。
The built-in profiler (the thing you see floating over your UI when you select “Show Perf Monitor” from the developer menu) isn’t all that comprehensive but it will give you a quick feel for whether a performance problem is happening on the JavaScript side of the app or the native side.
内置的事件探查器(当您从开发人员菜单中选择“显示Perf Monitor”时看到的漂浮在用户界面上的东西)虽然不那么全面,但是它可以使您快速了解是否在性能问题上发生了。应用程序JavaScript端或本机端。
If you think you have a problem with a specific component frequently re-rendering you can run your app with a debugger attached and try some targeted uses of console.count
in render functions. If you think some of your render functions are too expensive you could try using console.time
to time them.
如果您认为特定组件经常重新渲染存在问题,则可以在连接了调试器的情况下运行您的应用,并尝试在渲染函数中对console.count
进行一些有针对性的使用。 如果您认为某些渲染功能过于昂贵,则可以尝试使用console.time
对其计时。
That said, react-devtools lets you use the great React profiler and it will probably help you more quickly identify your problem.
就是说, react-devtools使您可以使用出色的React Profiler,它可能会帮助您更快地确定问题。
The React profiler will tell you in great detail where React is spending time rendering across the whole app, and it works in React Native. It also makes it easier to test changes and be confident that you have fixed the problem.
React探查器将详细告诉您React在哪里花时间在整个应用程序上渲染,并且它可以在React Native中工作。 它还使测试更改更加容易,并确信您已解决问题。
Note that you may have to use a slightly older version of the react-devtools if you aren’t on React Native 0.62 yet:
请注意,如果您尚未使用React Native 0.62,则可能必须使用较旧版本的react-devtools:
npm install -g react-devtools@^3
npm install -g react-devtools@^3
It probably is obvious but things to look out for are commits that took a long time and which components took the longest time to render in those commits. The Flame chart can give you a good overall picture of what components took the most time to render, while the Ranked chart is good for finding problems in a particular commit. Also if you are using PureComponent
as a rule, also look for components that seem to be re-rendered a lot when they shouldn't be.
这可能很明显,但是需要注意的是提交花费了很长时间,并且哪些组件花费了最长的时间来呈现这些提交。 Flame图表可以使您大致了解哪些组件花费了最多的时间进行渲染,而Ranked图表则有助于发现特定提交中的问题。 另外,如果您通常使用PureComponent
,还应寻找那些似乎不应该重新渲染的组件。
编写渲染测试 (Writing Rendering Tests)
Finally, if you have gone to the trouble of identifying and fixing a rendering problem it would be good to write a unit test to prevent it from recurring. This isn’t practical in all cases, but if you are using PureComponent
it can be pretty easy. Being able to do this easily is a great strength of React Native and the ecosystem.
最后,如果您遇到了识别和修复渲染问题的麻烦,那么最好编写一个单元测试以防止其再次发生。 这并非在所有情况下都可行,但是如果您使用的是PureComponent
则可能会很容易。 轻松做到这一点是React Native和生态系统的强大优势。
At Tableau we use react-native-testing-library to do this because it supports deep rendering (enzyme only supports shallow rendering on React Native).
在Tableau中,我们使用react-native-testing-library进行此操作,因为它支持深度渲染(酶仅支持React Native上的浅度渲染)。
For class-based components, writing such a test basically involves rendering either the component you want to test directly or a component that contains the one you want to test, spying on the render function of the component you want to test, triggering some action or calling a method that might cause re-rendering to happen, and finally checking whether re-rendering happened using the spy.
对于基于类的组件,编写这样的测试基本上涉及渲染要直接测试的组件或包含要测试的组件的组件,监视要测试的组件的渲染功能,触发某些操作或调用可能导致重新渲染发生的方法,最后使用间谍检查是否进行了重新渲染。
Here is an example test that makes sure dispatching a random Redux action doesn’t accidentally trigger re-rendering on MyPresentationalComponent
:
这是一个示例测试,可确保调度随机Redux操作不会意外触发MyPresentationalComponent
上的重新渲染:
结语 (Wrapping up)
Well, that is a lot to take in. Hopefully this will be useful to you when getting started on a new React Native app or dealing with performance problems in your current one. Getting native-like performance with React Native is, in our experience at Tableau, in large part just understanding how things work, and knowing what patterns to use and which pitfalls to avoid.
好吧,这有很多好处。希望这对您开始使用新的React Native应用程序或处理当前应用程序中的性能问题时将对您有用。 根据我们在Tableau的经验,使用React Native获得类似原生的性能在很大程度上只是了解事物的工作原理,并知道使用哪种模式以及避免哪些陷阱。
翻译自: https://engineering.tableau.com/react-native-performance-learnings-85f47ba38acb
响应式 性能