📜 ⬆️ ⬇️

How to avoid performance problems when creating React-applications


Performance react


React is not for nothing considered to be very productive framework. It allows you to create fast dynamic pages with a large number of elements.


But there are situations when there are a lot of elements on the page and react lacks the built-in performance. Then you have to apply various techniques for optimization.


A page written on react consists of individual components. Each of them is responsible for the appearance of a particular part of the page. This appearance depends on the parameters (properties) that were passed to the component. At occurrence of any events (for example, at any actions of the user or data acquisition on a network) properties can change. If the properties of a component have changed, it must be redrawn so that these changes are displayed on the user's screen.


If any events occur, parts of the page can be redrawn, even if the properties of the components that are responsible for them have not been changed. If there are many such parts, then for any user action, the drawing can take a considerable amount of time, and there can be noticeable delays when interacting with the page interface. The main way to deal with such performance problems is to cancel component redrawing, if their properties have not been changed. That is, upon the occurrence of any events, only those elements on the page should be redrawn, the properties and appearance of which this event affected. To do this, when creating react-components, you must define the method shouldComponentUpdate or use React.PureComponent as the parent class instead of React.Component.


The shouldComponentUpdate method should return false when no redraw is needed. And in the React.PureComponent class, this method is already implemented. It checks all incoming properties, and the component will not be redrawn if none of its properties have changed.


Example shouldComponentUpdate:


import * as React from 'react'; class Component extends React.Component { shouldComponentUpdate(nextProps) { return this.props.value !== nextProps.value } render() { return <div>...</div>; } } 

Example React.PureComponent:


 import * as React from 'react'; class Component extends React.PureComponent { render() { return <div>...</div>; } } 

When optimization doesn't work


The working principle of shouldCompenentUpdate and React.PureComponent is based on a comparison of old properties with new ones. If the properties have not changed, the appearance of the component is not updated.


Therefore, it is necessary to ensure that the properties are changed only when necessary.
But there are situations in which a component unintentionally gets new properties when it is not required.


This can occur when using the bind method, arrow functions, literal objects, or other constructs that create new objects with each call, in the render method.


When optimization doesn't work:


 class ParentComponent extends React.Component { _onClick() { doSomehing(); } render() { return ( <div> <Child1 onClick={this._onClick.bind(this)}/> <Child2 onClick={() => this._onClick()}/> <Child3 data={{id: this.props.id, value: this.props.value}}/> <Child4 items={this.props.items.map((item) => {.....})}/> </div> ); } } 

The bind and arrow functions each time a new function is returned.


Comparing bind results:


 this._onClick.bind(this) === this._onClick.bind(this) // false () => this._onClick() === () => this._onClick() //false 

Therefore, here the first two components will each time receive a new onClick property.
Child3 will receive the new object in the data property, and the fourth will receive the new array in items.


Such constructions should be avoided. When using them, performance optimizations made with shouldComponentUpdate or React.PureComponent will not work due to getting new properties.


In principle, you can write such an implementation of shouldComponentUpdate, in which changes of some properties will be ignored and the update will not occur. But this method can not be used in all situations. First, if there are many properties, such an implementation may contain a fairly large number of checks, and this may be the cause of errors and bugs. Also, this code is more difficult to maintain, as when adding new properties you will need to modify the method shouldComponentUpdate. Secondly, with this approach, the component in which shouldComponentUpdate is implemented must know exactly where and under what circumstances it will be used to determine exactly which changes of which particular properties can be ignored. And in the place where it will be used, you need to know its internal implementation, so as not to transfer once again new values ​​to those properties, the change of which will lead to unnecessary redrawing. This greatly impairs the encapsulation of components, which is often undesirable.


Performance Test Results


Often during the writing of code and during the code review there are doubts whether it is necessary to waste your time and pay attention to such problems. To determine the impact that the transfer of new properties to components has on performance, a small test page was written. It is located at https://megazazik.imtqy.com/react-perf-test/


The project allows to measure the update time of a large number of small components in situations where they receive new properties each time, as well as when the properties remain unchanged. For testing used different components:



When you open the project page, a list of thousands of items is displayed. When you click on any element or when you hover the mouse, the selected element becomes active, and the list is redrawn again. At the same time, depending on the selected parameters, elements whose state has not changed receive either the same properties or new ones, due to the use of switch functions.


1000 items update time:


Device, BrowserOptimizationPure, mcStateful, mcStateless, mchtml, mc
Desktop, Chromenot6.56.35.83.5
Desktop, ChromeYes3.34.94.32.5
Desktop, Firefoxnot13.711.911.57.3
Desktop, FirefoxYes5.48.57,64.4
Desktop, Edgenot19.616.213.58.6
Desktop, EdgeYes8.111.58.74.7
Mobile, Chromenot31.130.229.619.3
Mobile, ChromeYes16.725.821.514.8

Specific numbers can be highly dependent on the device and the browser on which the page is opened, as well as on the memory status and CPU usage. But the relative difference in rendering performance, with and without optimization, should always remain at about the same level.


You can compare the results with those that will be relevant for your device.


The optimal rendering time can be considered such a time at which the browser will not skip frames when updating the screen of the device. Most modern screens support a refresh rate of at least 60 frames per second. It turns out, the screen is updated at least every 16.6 milliseconds. Therefore, taking into account the possible additional load, it is desirable that the update of all components of the page takes place no more than 10 milliseconds.


According to the test results, it can be seen that the rendering time of components inherited from PureComponent and html elements is significantly reduced when the properties do not change.
Moreover, in tests, the immutability of properties also affects the performance of other types of components. But it is significantly less and is not connected with the abolition of redrawing, but with the fact that the properties they received are eventually transferred to the same div-elements in the markup, which are already in turn optimized.


When testing used the smallest components. As their size increases, the performance gain should only increase. This effect can be roughly estimated if the “Split List” option is enabled in the test project. In this mode, the entire list is divided into groups of 10 elements each. Then every 10 groups turn into groups of the top level. As a result, the page consists of 10 groups, which contain 10 subgroups, each of which has 10 elements.


Such a partition is more like what real pages consist of. Typically, a page contains several large components that contain smaller nested components.


In this mode, you can estimate the difference in performance between the worst situation, when in any event every element of the page is redrawn, and the situation. when only those elements are updated, the properties that have changed.


Device, BrowserOptimizationPure, mcStateful, mcStateless, mchtml, mc
Desktop, Chromenot8.78.38.06.1
Desktop, ChromeYes0.90.90.90.8
Desktop, Firefoxnot19.116.315.412.6
Desktop, FirefoxYes1.51.51.51,3
Desktop, Edgenot23.120.618.113.0
Desktop, EdgeYes2.92.92.92.8
Mobile, Chromenot37.135.034.524.9
Mobile, ChromeYes5.25.65.34.7

In this case, the type of the component under test is of little importance, since the groups to which the list is divided implement the own method shouldComponentUpdate.


How to avoid creating new features in render


In practice, most often unnecessary generation of new component properties occurs due to the use of switch functions or the bind function. Consider how you can solve this problem. Most often, it is quite simple. But sometimes you need to spend some time.


The easiest way to not create new functions with each update is to create them once at the moment of initialization of the component instance.


To do this, you can call the bind function not in the render method, but in the class constructor, and save the result in an object property for later reference in the render method.


Example bind in constructor:


 class ParentComponent extends React.Component { constructor(props) { super(props); this._onClick = this._onClick.bind(this); } _onClick() { doSomehing(); } render() { return <ChildComponent onClick={this._onClick}/>; } } 

Or you can use the arrow function and also save the result in the property of the object.


Example with arrow function:


 class ParentComponent extends React.Component { _onClick = () => { doSomehing(); } render() { return <ChildComponent onClick={this._onClick}/>; } } 

The previous method is simple and fast to implement. It should be used in most cases.


But there are situations when the callback function is transferred to the list of child components, for example, in a loop, and when it is executed, it must be known in which of the elements the call occurred. Such a function can take as an argument, for example, the index of an element of an array.


Example with a list:


 class ParentComponent extends React.Component { _onClick = (index) => { doSomehing(index); } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildComponent onClick={() => this._onClick(index)} item={item} /> ))} </div> ); } } class ChildComponent extends React.PureComponent { render() { return <div onClick={this.props.onClick}>...</div>; } } 

Here each child component from the list should get its unique function, in which the element index is stored through the closure.


In this case, getting rid of creating new functions with each drawing is more difficult. There are several ways to do this.


Changing the interface of the child component


To use the first approach, you need to create only one callback function and pass it to all child components. At the same time, the responsibility for passing the necessary arguments to the function will rest on the child components.


An example with id transfer:


 class ParentComponent extends React.Component { _onClick = (id) => { doSomehing(id); } render() { return ( <div> {this.props.items.map((item) => ( <ChildComponent onClick={this._onClick} item={item} /> ))} </div> ); } } class ChildComponent extends React.PureComponent { _onClick = () => { this.props.onClick(this.props.item.id) } render() { return <div onClick={this._onClick}>...</div>; } } 

If there is no data required for the callback function inside the child component, then it will need to be passed to it through the properties.


An example with adding index to properties:


 class ParentComponent extends React.Component { _onClick = (index) => { doSomehing(index); } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildComponent onClick={this._onClick} item={item} index={index} /> ))} </div> ); } } class ChildComponent extends React.PureComponent { _onClick = () => { this.props.onClick(this.props.index) } render() { return <div onClick={this._onClick}>...</div>; } } 

This method is not always possible to use. First, we may not have access to the source code of the child component. Secondly, adding new properties may be undesirable due to the fact that this may adversely affect the clarity and integrity of its interface.


Component selection


If you cannot use the previous method or if this is undesirable, you can create a separate component that will be responsible for passing an additional parameter to the callback function.


Example selection component:


 class ParentComponent extends React.Component { _onClick = (index) => { doSomehing(index); } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildWrapperComponent onClick={this._onClick} item={item} index={index} /> ))} </div> ); } } class ChildWrapperComponent extends React.PureComponent { _onClick = () => { this.props.onClick(this.props.index) } render() { return ( <ChildComponent onClick={this._onClick} item={item} /> ); } } 

This method can be used in almost any situation. Perhaps his only drawback is that it is relatively laborious. You have to write a separate class only to get rid of creating functions when you call render.


Passing Values ​​as Properties to Native Elements


A method can also be used in which the array index (or other key) is passed to child html elements (such as, for example, button and input), and when an event occurs in the callback function, this data is extracted from the html element.


Example with html elements:


 class ParentComponent extends React.Component { _onClick = (e) => { doSomehing(e.target.value); } render() { return ( <div> {this.props.items.map((item, index) => ( <button type="button" onClick={this._onClick} value={index} > {item.title} </button> ))} </div> ); } } 

We cannot implement shouldComponentUpdate for html elements, but, as tests have shown, they have a built-in optimization mechanism and the immutability of their properties can also significantly reduce the update time of such elements.


Caching Callbacks


To use this method, you need to create a list of callback functions once for all child components, and use the previously created functions for subsequent rendering. One of the options for such an implementation:


Caching Kolbeks:


 class ParentComponent extends React.Component { _callbacks = {}; _getOnClick = (index) => { if (!this._callbacks[index]) { this._callbacks[index] = () => doSomehing(index); } return this._callbacks[index]; } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildComponent onClick={this._getOnClick(index)} item={item} /> ))} </div> ); } } 

This method is relatively time consuming. If, in addition to the index, you need to transfer some more data to the callback function, then for its proper operation you will need to implement more complex logic and write more code. In addition, this function will need to be copied to each component where it is needed, and, possibly, to paste this code several times into the same class. Therefore, to avoid duplication of code in each class, you can create a module that would perform the necessary actions.


For an example of such modules, there are two packages:



Both packages have the same features, but they have a different interface.
They allow you to create a callback function once for each child component from the list and use it with each subsequent drawing.


An example of using cached-bind:


 import bind from 'cached-bind'; class ParentComponent extends React.Component { _onClick(index) { doSomehing(index); } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildComponent onClick={bind(this, '_onClick', index)} item={item} /> ))} </div> ); } } 

An example of using react-cached-callback:


 import cached from 'react-cached-callback'; class ParentComponent extends React.Component { @cached _getOnClick(index) { return () => doSomehing(index); } render() { return ( <div> {this.props.items.map((item, index) => ( <ChildComponent onClick={this._getOnClick(index)} item={item} /> ))} </div> ); } } 

To understand what kind of stored function must be returned, both packages must determine the ID of the child component when they are called. As an identifier, you can use, for example, the index of an element in an array.


cached-bind should get this identifier as the third argument when calling the bind function.
And react-cached-callback by default considers the first argument passed to the original function to be the identifier. You can find out the details of the interfaces in the package description.


Finally


React is a fast framework, and much of the applications will work with sufficient performance without additional effort. But, when increasing the size of the application, the developer may need to apply various optimization techniques and make sure that the properties of the components are not changed unnecessarily.


Many libraries for state management (for example, redux or MobX) currently have their own methods for optimizing react-components. But even with their use, the optional generation of new objects and functions when drawing components can lead to significant delays when updating the user interface.


')

Source: https://habr.com/ru/post/348584/


All Articles