📜 ⬆️ ⬇️

Caching event handlers and improving the performance of React applications

Today we publish a translation of the material, the author of which, after analyzing the features of working with objects in JavaScript, offers React-developers a technique for accelerating applications. In particular, we are talking about the fact that a variable, which, as they say, “is assigned an object”, and which is often called simply “an object”, in fact, does not store the object itself, but a link to it. Functions in JavaScript are also objects, so the above is also true for them. If you keep this in mind by designing React components and critically analyzing their code, you can improve their internal mechanisms and improve application performance.



Features of working with objects in JavaScript


If you create a couple of functions that look exactly the same, and try to compare them, it turns out that they are, from the point of view of the system, different. In order to verify this, you can run the following code:

const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false 

Now we will try to assign a variable to an already existing function, which is already assigned to another variable, and compare these two variables:
')
 const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true 

As you can see, with this approach, the strict equality operator yields true .
Objects, of course, behave in the same way:

 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true 

Here we are talking about JavaScript, but if you have experience developing in other languages, then you may be familiar with the concept of pointers. In the above code, every time an object is created, a section of system memory is allocated for it. When we use a command like object1 = {} , this results in filling with some data a section of memory allocated specifically for object1 .

It is quite possible to imagine object1 in the form of an address at which data structures related to an object are located in memory. Running the object2 = {} command results in the allocation of another memory area specifically designed for object2 . Are obect1 and object2 in the same memory area? No, each of them is allocated its own section. That is why when we try to compare object1 and object2 we get false . These objects may have an identical structure, but the addresses in the memory where they are located are different, and when comparing, the addresses are checked.

By executing the command object3 = object1 , we write the object3 address of object1 into the constant object1 . This is not a new object. This constant is assigned the address of an existing object. You can check it like this:

 const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x; // false 

In this example, an object is created in memory and its address is written to the object1 constant. Then in the object3 constant the same address is written. Changing object3 leads to a change in the object in memory. This means that when accessing an object using any other link to it, for example, the one that is stored in object1 , we will work with its modified version.

Functions, Objects and React


Failure to understand the above mechanism by novice developers often leads to errors, and, perhaps, consideration of the features of working with objects is worthy of a separate article. However, our current theme is the performance of React applications. Even experienced developers can make mistakes in this area, who simply do not pay attention to how React applications are affected by the fact that JavaScript variables and constants store not the objects themselves, but only references to them.

What does this have to do with React? React has intelligent mechanisms for saving system resources aimed at improving application performance: if the properties and state of the component do not change, then what the render function displays will not change either. Obviously, if the component remains the same, it does not need to be re-rendered. If nothing changes, the render function will return the same as before, so there is no need to execute it. This mechanism makes React fast. Something is displayed only when necessary.

React checks the properties and state of the components for equality using the standard JavaScript capabilities, that is, it simply compares them using the == operator. React does not perform "shallow" or "deep" comparison of objects in order to determine their equality. A “shallow comparison” is a concept used to describe the comparison of each key-value pair of an object, as opposed to a comparison, in which only the addresses of objects in memory (references to them) are compared. With a “deep comparison” of objects go even further, and if the value of the object properties being compared are also objects, the comparison of the key-value pairs of these objects is also performed. This process is repeated for all objects nested in other objects. React does nothing of the kind, performing only a check for equality of links.

If you, for example, change the property of a certain component represented by an object of the form { x: 1 } to another object that looks exactly the same, React will re-render the component, since these objects are in different memory areas. If you recall the above example, then if you change the property of a component from object1 to object3 , React will not re-render such a component, since the object1 and object3 refer to the same object.

Working with functions in JavaScript is organized in the same way. If React encounters the same functions, the addresses of which differ, it will re-render. If the “new function” is just a link to a function that has already been used, there will be no re-rendering.

Typical problem when working with components


Here is one of the variants of the scenario of working with components, which, unfortunately, I constantly come across when checking someone else's code:

 class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={() => alert('!')} />     </div>   ); } } 

Before us is very simple component. It is a button that, when clicked, displays a notification. Next to the button displays instructions for its use, telling the user whether to press this button. They control which instruction will be displayed by setting the do ( do={true} or do={false} ) SomeComponent component.

Each time the SomeComponent component is re-rendered (when the value of the do property changes from true to false and vice versa), the Button element is rendered again. The onClick handler, although it is always the same, is created anew each time the render function is called. As a result, it turns out that every time a component is created in memory, a new function is created, since its creation is performed in the render function, the link to the new address in memory is passed to <Button /> , and the Button component is also rendered again, despite the fact that nothing has changed at all.

Talk about how to fix it.

Solution to the problem


If the function does not depend on the component (on the this context), then you can define it outside the component. All instances of the component will use the same function reference, since in all cases it will be the same function. Here's what it looks like:

 const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={createAlertBox} />     </div>   ); } } 

Unlike the previous example, createAlertBox , each time render called, will contain the same reference to the same area in memory. As a result, Button will not be Button again.

While the Button component is small and quickly rendered, the above-described problem associated with the internal declaration of functions can also be found in large, complex components that take a long time to complete. This can significantly slow down the React application. In this regard, it makes sense to follow the recommendations, according to which such functions should never be declared inside the render method.

If the function depends on the component, that is, it cannot be defined outside its limits, the component method can be passed as an event handler:

 class SomeComponent extends React.PureComponent { createAlertBox = () => {   alert(this.props.message); }; get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={this.createAlertBox} />     </div>   ); } } 

In this case, in each instance of SomeComponent when you press a button, various messages will be displayed. The event handler for a Button element must be unique to SomeComponent . When passing the cteateAlertBox method, cteateAlertBox doesn’t matter whether cteateAlertBox will be re-rendered. It doesn't matter if the message property has changed. The address of the createAlertBox function createAlertBox not change, which means that the Button element should not be re-rendered. This allows you to save system resources and improve the rendering speed of the application.

All this is good. But what if the functions are dynamic?

Solving a more complex problem


The author of this material asks to pay attention to the fact that he prepared the examples given in this section, taking the first thing that came to his mind, suitable to illustrate the reuse of functions. These examples are intended to help the reader grasp the essence of the idea. Although this section is recommended for reading to understand the essence of what is happening, the author advises to pay attention to the comments on the original article , as some readers suggested there more advanced options for implementing the mechanisms considered here, which take into account the features of cache invalidation and the memory management mechanisms built into React.

So, the situation when in one component there are many unique, dynamic event handlers is extremely common, for example, something like this can be seen in the code where the map array method is used in the render method:

 class SomeComponent extends React.PureComponent { render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={() => alert(listItem.text)} />         </li>       )}     </ul>   ); } } 

There will be displayed a different number of buttons and create a different number of event handlers, each of which is represented by a unique function, and, in advance, when creating SomeComponent , it is unknown what these functions will be. How to solve this puzzle?

Here memoization will help us, or, to put it simply, caching. For each unique value, you need to create a function and put it in the cache. If this unique value is encountered again, it will suffice to take from the cache its corresponding function, which was previously placed in the cache.

Here is the implementation of this idea:

 class SomeComponent extends React.PureComponent { //    SomeComponent        //   . clickHandlers = {}; //       //    . getClickHandler(key) {   //       ,  .   if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {     this.clickHandlers[key] = () => alert(key);   }   return this.clickHandlers[key]; } render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={this.getClickHandler(listItem.text)} />         </li>       )}     </ul>   ); } } 

Each array element is processed by the getClickHandler method. This method, when first called with a certain value, will create a function unique to this value, put it in the cache and return it. All subsequent calls to this method with passing it the same value will result in it simply returning the function reference from the cache.

As a result, re-rendering SomeComponent will not re-render the Button . Similarly, adding items to the list property will result in the dynamic creation of event handlers for each button.

You will need to be creative when creating unique identifiers for handlers if they are defined by more than one variable, but this is not much more complicated than the usual creation of a unique key property for each JSX object resulting from the map method.

Here I would like to warn you about possible problems of using array indexes as identifiers. The fact is that with this approach you may encounter errors if the order of the elements in the array changes or some of its elements are deleted. So, for example, if at first this array looked like [ 'soda', 'pizza' ] , and then turned into [ 'pizza' ] , and you cached event handlers with the command like listeners[0] = () => alert('soda') , you will find that when a user clicks on a button to which a handler is assigned with ID 0, and which, according to the contents of the [ 'pizza' ] array, should display the message pizza , the message soda will be displayed. For the same reason, it is not recommended to use array indices as properties that are keys.

Results


In this article, we explored the features of the internal mechanisms of JavaScript, given that you can speed up the rendering of React-applications. We hope the ideas presented here will be useful to you.

Dear readers! If you know any interesting ways to optimize React-applications, please share them.

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


All Articles