Hello, Habr! My name is Kamo Spertsyan, I am engaged in React Native development at Profi.ru. If you decide to use React Native technology to quickly deliver product features and focus on development speed, then you are likely to run into performance issues. At least that's what happened to us. After six months of active development, the performance of our application fell below a critical level - everything was wildly slow. Therefore, we took up the optimization - removed all the “brakes” during startup, transitions between screens, rendering screens, reactions to user actions. As a result, in three months they brought the user experience to the native level. In this article I want to talk about how we optimized the application on React Native and solved the problem of multiple component renders.
I put together recommendations that will help minimize the number of pointless redraws of components. For clarity, in the examples I compare the “bad” and “good” implementations. The article will be useful to those who are already faced with poor application performance, and those who do not want to allow this in the future.We use React Native paired with Redux. Some of the tips are related to this library. Also in the example, I use the Redux-thunk library - to simulate working with the network.When to think about performance?
In fact, it is worth remembering from the very beginning of work on the application. But if your application is already slowing down - do not despair, everything can be fixed.Everyone knows, but just in case, I’ll mention: it’s better to check performance on weak devices. If you are developing on powerful devices, you may not be aware of the “brakes” of end users. Decide for yourself the devices you will be guided by. Measure time or FPS in control plots to compare with results after optimization.React Native out of the box provides the ability to measure FPS applications through Developer Tools → Show perf monitor. The reference value is 60 frames per second. The lower this indicator, the stronger the application "slows down" - does not respond or reacts with a delay to user actions. One of the main effects on FPS is the number of renders, the “severity” of which depends on the complexity of the components.Example description
I show all the recommendations on the example of a simple application with a list of news. The application has one screen, which is located FlatList
with the news. A news item is a component NewsItem
that consists of two smaller components - the headline ( NewsItemTitle
) and the body ( NewsItemBody
). The entire example can be seen here . Further in the text are links to various branches of the repository for specific examples. The repository is used for the convenience of readers who want to explore examples more deeply. The code in the repository and examples below does not claim to be perfect - it is needed solely for demonstration purposes.Below, all components are schematically shown with links and props.In the render method of each component, I added the output to the console of unique information about it:SCREEN
ITEM_{no}
ITEM_TITLE_{no}
ITEM_BODY_{no}
where {no}
is the news serial number in order to distinguish between different news renderings from multiple renderings of the same one.For testing on each refresh
news list, additional news is added to its beginning. At the same time, the following message is displayed in the console:--------------[ REFRESHING ]--------------
These records will help to understand if there is a problem in any particular component, and later to determine whether it was possible to optimize it.If implemented correctly, our log after launch and several updates should look like this:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
--------------[ REFRESHING ]--------------
SCREEN
ITEM_4
ITEM_TITLE_4
ITEM_BODY_4
At the first start, the screen itself and two initial news are drawn. When updating the board, the screen is rendered again, because its data has really changed. More news comes up. All previous news is not redrawn, as there was no change in their data.When is a component rendered?
In React and React Native, there are two conditions for rendering a component:- changing his props / state,
- render of the parent component.
A function can be redefined in a component shouldComponentUpdate
- it receives new Props and State as an input and tells whether the component should be rendered. Often, to avoid unnecessary re-renders, a shallow compare of Props and State objects is enough. For example, this eliminates unnecessary renders when the parent component changes, if they do not affect the child component. In order not to write a surface comparison manually each time, you can inherit a component from React.PureComponent
that encapsulates this check.When we use the connect link function, the Redux library creates a new component that is “connected” to the global State. Changes to this State trigger a methodmapStateToProps
which returns new props. Next, a comparison of old and new props starts, regardless of whether the component was declared as PureComponent
or not.Consider these nuances in our example.We will NewsItem
let the component pass through connect
, NewsItemTitle
inherit from React.Component
, and NewsItemBody
- from React.PureComponent
.→ Full example codeexport class NewsItemTitle extends React.Component
export class NewsItemBody extends React.PureComponent
Here's what the log will look like after one board update:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
You can see that the news and headline components are redrawn. We will consider them in turn.NewsItem
declared using connect
. As a props, this component receives an identifier, by which it subsequently receives news in mapStateToProps
:const mapStateToProps = (state, ownProps) => ({
item: state.newsMap[ownProps.itemKey],
});
Since when updating the board all the news is downloaded again, the object will item
update and afterwards will refer to various memory cells. In other words, they will be different objects, even if all the contained fields are the same. Therefore, a comparison of the previous and new State'ov component returns false
. The component will be re-rendered, despite the fact that in fact the data has not changed.NewsItemTitle
is inherited from React.Component
, so it is re-rendered every time the parent component is rendered. This happens regardless of the values ​​of old and new props.NewsItemBody
inherited from React.PureComponent
, so it compares old and new props. In news 1 and 2, their values ​​are equivalent, therefore the component is rendered only for news 3.To optimize the renderingsNewsItemTitle
just declare it as React.PureComponent
. In the case of, you have NewsItem
to redefine the function shouldComponentUpdate
:shouldComponentUpdate(nextProps) {
return !shallowEqual(this.props.item, nextProps.item);
}
→ Full example codeHere shallowEqual
is a feature for surface comparison of objects that Redux provides. You can write like this:shouldComponentUpdate(nextProps) {
return (
this.props.item.title !== nextProps.item.title ||
this.props.item.body !== nextProps.item.body
);
}
Here's what our log will look like after this:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
NoteshouldComponentUpdate
NewsItem
, NewsItemTitle
. . NewsItemTitle
- NewsItem
, .
React.memo and functional components
It is shouldComponentUpdate
not possible to override in a functional component. But this does not mean that in order to optimize a functional component, you have to rewrite it into a class one. For such cases, the React.memo memoization function is provided . It accepts a component input and an optional comparison function areEqual
. When called, it areEqual
gets old and new props and should return the result of the comparison. The difference with shouldComponentUpdate
what areEqual
should return true
if the props are equal, and not vice versa.For example, NewsItemTitle
memoization may look like this:areEqual(prevProps, nextProps) {
return shallowEqual(prevProps, nextProps);
}
export OptimizedNewsItemTitle = React.memo(NewsItemTitle, areEqual)
If you do not pass areEqual
in React.memo
, then a superficial comparison of props will be made, so our example can be simplified:export OptimizedNewsItemTitle = React.memo(NewsItemTitle)
Lambda functions in props
To process component events, functions can be passed to its props. The most striking example is implementation onPress
. Often anonymous lambda functions are used for this. Let's say in NewsItemBody
we want to show only the preview, and if you click on it - the whole text. To do this, when rendering NewsItem
in, NewsItemBody
we will pass the following prop:<NewsItemBody
...
onPress={() => this.props.expandBody()}
...
/>
Here's what the log looks like with this implementation when the method shouldComponentUpdate
is NewsItem
deleted:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
News bodies 1 and 2 are rendered, although their data has not changed, but NewsItemBody
is PureComponent
. This is due to the fact that for each render the NewsItem
value of props onPress
is created anew. Technically, onPress
with each render, it points to a new area in memory, so a superficial comparison of props in NewsItemBody
returns false. The problem is fixed by the following entry:<NewsItemBody
...
onPress={this.props.expandBody}
...
/>
Log:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
→ Full example codeUnfortunately, an anonymous function can by no means always be rewritten as a method or a class field for such a record. The most common case is when inside the lambda function the scope variables of the function in which it is declared are used.Consider this case in our example. To switch from the general list to the screen of one news, we add the processing of clicking on the body of the news. The renderItem
component method FlatList
will look like this:const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={() => this.onItemBodyPress(item)}
/>
);
An anonymous function onBodyPress
cannot be declared in a class, because then the variable item
that is needed to go to a specific news will disappear from the scope .The simplest solution to the problem is to change the signature of the onBodyPress
component props NewsItem
so that the required parameter is passed to the function when called. In this case, this is the news identifier.const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={item => this.onItemBodyPress(item)}
/>
);
In this case, we can already take out the anonymous function in the component class method.const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={this.onItemBodyPress}
/>
);
However, such a solution will require us to change the component NewsItem
.class NewsItemComponent extends React.Component {
render() {
...
return (
...
<NewsItemBody
...
onPress={() => this.props.onBodyPress(this.props.item)}
...
/>
...
);
}
And again we return to the indicated problem - we pass a new lambda function to the child component for each render of the parent. Only now have we gone down a level. Log:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
To get rid of this problem at the root, you can use the useCallback hook . It allows memoizing a function call with passing an argument. If the argument of the function does not change, then the result of the call useCallback
will point to the same area of ​​memory. In our example, this means that when redrawing the same news, the prop onPress
component NewsItemBody
will not change. Hooks can only be used in functional components, so the final look of the component NewsItem
will be as follows:function NewsItemComponent(props) {
...
const {itemKey, onBodyPress} = props.item;
const onPressBody = useCallback(() => onBodyPress(itemKey), [itemKey, onBodyPress]);
return (
<View>
...
<NewsItemBody
...
onPress={onPressBody}
...
/>
</View>
);
}
And the log:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
→ Full example codeArrays and objects
In JavaScript, functions are represented as objects, along with arrays. Therefore, the example from the previous block is a special case of creating a new object in props. It is quite common, so I put it in a separate paragraph.Any creation of new functions, arrays or objects in props leads to a component re-renderer. Consider this rule in the following example. Let's pass in a NewsItemBody
combined style of two values:<NewsItemBody
...
style={[styles.body, styles.item]}
...
/>
And again, the log shows the extra component renderers:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
To solve this problem, you can select a separate style that will combine body
and item
, or, for example, move the declaration of the array [styles.body, styles.item]
into a global variable.→ Full example codeArray reducers
Consider another popular source of “brakes” associated with use FlatList
. A classic application that contains a long list of items from the server implements pagination. That is, it loads a limited set of elements in the form of the first page, when the list of current elements ends, it loads the next page, and so on. An item list reducer might look like this:const newsIdList = (state = [], action) => {
if (action.type === 'GOT_NEWS') {
return action.news.map(item => item.key);
} else if (action.type === 'GOT_OLDER_NEWS') {
return [...state, ...action.news.map(item => item.key)];
}
return state;
};
When each next page loads in the application’s style, a new array of identifiers is created. If we pass this array to props later FlatList
on, here is what the component render logs will look like:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..10>
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
ITEM_<1..30>
For this example, I made a few changes in the test application.- Set the page size to 10 news.
item
NewsItem
FlatList
-, connect. NewsItem
React.Component
.- .
- . â„–1 .
The example shows that when each next page loads, all the old elements are rendered again, then the old elements and the elements of the new page are rendered again. For mathematics lovers: if the page size is equal X
, then when the i
ith page is loaded, instead of rendering only X
new elements, the elements are rendered (i - 1) * X + i * X
.“Ok,” you say, “I understand why all the elements are drawn after adding a new page: the reducer returned a new array, a new area of ​​memory, all that. But why do we need to render the old list before adding new elements? ” “Good question,” I will answer you. This is a consequence of working with the state of the component VirtualizedList
on whose basisFlatList
. I will not go into details, as they pull on a separate article. Who cares, I advise you to delve into the documentation and source.How to get rid of such non-optimality? We rewrite the reducer so that he does not return a new array for each page, but adds elements to the existing one:Attention! Antipattern!. , , ,
PureComponent
, . , . .
Redux.
const newsIdList = (state = [], action) => {
if (action.type === 'GOT_NEWS') {
return action.news.map(item => item.key);
} else if (action.type === 'GOT_OLDER_NEWS') {
action.news.forEach(item => state.push(item.key));
return state;
// return [...state, ...action.news.map(item => item.key)];
}
return state;
};
After that, our log will look like this:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..30>
We got rid of the rendering of old elements before adding elements to a new page, but old elements are still drawn after updating the list. The number of renderings for the next page is now equal i * X
. The formula has become simpler, but we will not stop there. We only have X
new elements, and we only want X
new renders. We will use the already familiar tricks to remove news renders that have not changed props. Return connect to NewsItem
:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<11..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<21..30>
Fine! Now we can be satisfied with ourselves. There is nowhere to optimize.→ Full example codeAn attentive reader will indicate that after applying connect to the NewsItem
log, it will look like in the last example, no matter how you implement the reducer. And it will be right - if the news component checks its props before rendering, it does not matter if the old array is used by the reducer or it creates a new one. Only new elements are drawn and only once. However, changing the old array instead of creating a new one saves us from unnecessary renderings of the component FlatList
used in it VirtualizedList
and unnecessary iterations of props equivalence checks NewsItem
. With a large number of elements, this also gives a performance increase.Use mutable arrays and objects in reducers should be with extreme caution. In this example, this is justified, but if you have, say, normal PureComponent
, then when you add elements to the mutable array, the components will not be rendered. Its props in fact remain unchanged, since before and after updating the array points to the same memory area. This can lead to unexpected consequences. No wonder the described example violates the principles of Redux .And something else...
If you use presentation-level libraries, I advise you to make sure that you understand in detail how they are implemented. In our application, we use a component Swipeable
from the library react-native-gesture-handler
. It allows you to implement a block of additional actions when swiping a card from the list.In code, it looks like this:<Swipeable
...
renderRightActions={this.renderRightActions}
...
>
Method renderRightActions
or renderLeftActions
returns the component that is displayed after the swipe. We determined and changed the height of the panel during the change of components in order to fit the necessary content. This is a resource-intensive process, but if it occurs during the swipe animation, the user does not see interference.The problem is that the component Swipeable
calls the method renderRightActions
at the time of rendering the main component. All calculations and even rendering of the action bar, which is not visible before the swipe, occur in advance. So, all these actions are performed for all cards in the list at the same time. This caused significant “brakes” when scrolling the board.The problem was solved in the following way. If the action panel is drawn together with the main component, and not as a result of the swipe, then the method renderRightActions
returns empty the View
size of the main component. Otherwise, we draw the panel of additional actions as before.I give this example because supporting libraries do not always work as you expect. And if these are presentation-level libraries, then it is better to make sure that they are not wasting unnecessary resources.findings
After eliminating the problems described in the article, we significantly accelerated the application on React Native. Now it is difficult to distinguish it in performance from a similar one, implemented natively. Excess renders slowed down both the loading of individual screens and the reaction to user actions. Most of all, it was noticeable on the lists, where dozens of components are drawn at once. We have not optimized everything, but the main screens of the application no longer slow down.The main points of the article are briefly listed below.- React Native : Props/State- .
- ,
React.PureComponent
, , . - ,
shouldComponentUpdate
React.Memo
. - - . , (shallow compare). , .
- Supporting presentation-level libraries can lead to unexpected waste of resources. It is worth being careful in their application.
That's all. I hope you find the information useful. I will be glad to any feedback!Useful sources
- Understanding Rendering in React + Redux
- Comparing Objects in JavaScript
- Improving Performance in React Functional Components using React.memo ()
- How Discord achieves native iOS performance with React Native