📜 ⬆️ ⬇️

React, built-in functions and performance

When I have to talk about React, or when I give the first lecture of the training course, showing all sorts of interesting things, someone will certainly ask: “Built-in functions? I heard they are slow. ”



This question did not always appear, but in the past few months I, in the role of library author and teacher, have to answer it almost every day, sometimes in lectures, sometimes in twitter. Honestly, I'm already tired of this. Unfortunately, I did not immediately realize that it is better to present everything in the form of an article, which I hope will be useful for those who ask questions of performance. Actually - in front of you is the fruit of my labors.

What is a "built-in function"?


In the context of React, what is called an inline function is a function that is defined in the rendering process. In React, there are two meanings of the concept of rendering, which are often confused. The first relates to getting React elements from components (calling the components render methods) during the update process. The second is the actual update of the page fragments by modifying the DOM. When I talk about “rendering” in this article, I mean the first option.
')
Here are some examples of built-in functions:

 class App extends Component { // ... render() {   return (     <div>             {/* 1.    " DOM" */}       <button         onClick={() => {           this.setState({ clicked: true })         }}       >         Click!       </button>             {/* 2. " "  "" */}       <Sidebar onToggle={(isOpen) => {         this.setState({ sidebarIsOpen: isOpen })       }}/>             {/* 3.   render */}       <Route         path="/topic/:id"         render={({ match }) => (           <div>             <h1>{match.params.id}</h1>}           </div>         )       />     </div>   ) } } 

Premature optimization is the root of all evil


Before we continue, we need to talk about how to optimize programs. Ask any performance professional and he will tell you that premature optimization is evil. This applies to absolutely all programs. Anyone who knows how to optimize can confirm this.

I remember the speech of my friend Ralph Holzmann on gzip , which really strengthened this idea in me. He talked about an experiment that he did with LABjs , the old library for loading scripts. You can watch this performance . What I’m talking about here takes about two and a half minutes, starting with the 30th minute of the video.

At that time, something strange was done in LABjs , aimed at optimizing the size of the finished code. Instead of using the usual object notation ( obj.foo ), it used to store keys in strings and use square brackets to access the contents of objects ( obj[stringForFoo] ). The reason for this was that after minifying and compressing code with gzip unusually written code would have to become less than code that was written in a familiar way.

Ralph forked this code and removed the optimization, rewriting it in the usual way, without thinking about how to optimize the code for minification and gzip-compression.

It turned out that getting rid of "optimization" has reduced the size of the final file by 5.3%! Obviously, the author of the library wrote it immediately in an “optimized” form, without checking whether it would give any advantages. Without measurements, it is impossible to know whether something improves certain optimization. In addition, if the optimization only worsens the situation, you will not know about it either.

Not only can premature optimization significantly increase development time, degrade the purity of the code, it can have negative consequences and lead to problems, as it was with LABjs. If the author of the library took measurements instead of imagining performance problems, he would save development time, release a cleaner code with better performance.

I will quote this tweet here: “It annoys me when people, lounging in a chair, argue that a certain code will be slow to solve their problems, without taking any measurements of performance.” I support this point of view.

So, I repeat - do not engage in premature optimization. And now - back to React.

Why say built-in functions degrade performance?


Built-in functions are considered slow for two reasons. Firstly, this is due to concerns about memory consumption and garbage collection. Secondly, due to shouldComponentUpdate . Let's sort out these concerns.

â–Ť Memory consumption and garbage collection


To begin with, programmers (and estlint configurations ) are concerned about memory consumption and system load from garbage collection when creating built-in functions. This is the legacy of the days when the switch functions in JS were not yet widespread. If in the React code, in embedded constructions, the bind command was often used, this, historically, led to poor performance. For example:

 <div> {stuff.map(function(thing) {   <div>{thing.whatever}</div> }.bind(this)} </div> 

Problems with Function.prototype.bind were corrected here , and the switch functions were either used as built-in features of the language, or transpiled with the help of babel into normal functions. And so and so we can assume that they are not slow.

Remember not to make the assumption that some code will be slow. Write the code as you always do and measure the performance. If you can find any problems - fix them. You do not need to prove that the switch functions work quickly - let someone else prove that they are slow. Otherwise it is a premature optimization.

As far as I know, no one has yet led a study of his application, indicating that the built-in functions lead to performance problems. Up to this point it is not even worth talking about it, however, I, in any case, will share here one more idea.
If the load on the system from creating the built-in function is high enough to create a special eslint rule to prevent this, why would we strive to move these heavy operations to a very important initialization unit from the point of view of the impact on the speed of the system?

 class Dashboard extends Component { state = { handlingThings: false } constructor(props) {   super(props)     this.handleThings = () =>     this.setState({ handlingThings: true })   this.handleStuff = () => { /* ... */ }   //       bind   this.handleMoreStuff = this.handleMoreStuff.bind(this) } handleMoreStuff() { /* ... */ } render() {   return (     <div>       {this.state.handlingThings ? (         <div>           <button onClick={this.handleStuff}/>           <button onClick={this.handleMoreStuff}/>         </div>       ) : (         <button onClick={this.handleThings}/>       )}     </div>   ) } } 

Being engaged in preliminary optimization, we slowed down the initialization of the component three times. If all event handlers were built-in functions, the original call to render need to create only one function. Instead, we create three. Moreover, no measurement of performance was not carried out, so we have no reason to consider this a problem.

However, again, you should not get involved in the idea of ​​transferring everything that is necessary and not necessary to the built-in functions. If, inspired by the above idea, someone decides to create an eslint rule that will require the ubiquitous use of built-in functions to speed up the initial rendering, then we will end up with all the same harmful premature optimization.

â–ŤPureComponent and shouldComponentUpdate


The real essence of the problem lies in PureComponent and shouldComponentUpdate . In order to intelligently engage in performance optimization, you need to understand two things: the features of shouldComponentUpdate , and how the comparison for strict equality works in JavaScript. Not understanding these concepts, you can, trying to make the code faster, only make things worse.

When setState is setState , React compares the old element with the new one (this is called reconciliation ) and then uses the resulting information to update the elements of the real DOM. Sometimes this operation can happen rather slowly if there are too many items to check (something like a large SVG). In such cases, React provides a workaround called shouldComponentUpdate .

 class Avatar extends Component { shouldComponentUpdate(nextProps, nextState) {   return stuffChanged(this, nextProps, nextState)) } render() {   return //... } } 

If a component is set to shouldComponentUpdate , before React compares the old and the new elements, it will turn to shouldComponentUpdate to find out if something has changed. If the response returns false , React completely skips the element comparison operation, which saves some time. If the component is large enough, this can lead to a noticeable effect on performance.

The most common way to optimize a component is the React.PureComponent extension instead of React.Component . PureComponent will compare the properties and state in shouldComponentUpdate , as a result, you will not have to do it yourself.
class Avatar extends React.PureComponent { ... }

The Avatar class now uses strict equality comparisons when working with properties and state before requesting updates. It can be expected that this will speed up the program.

â–ŤComparison on strict equality


There are six primitive types in JavaScript: string , number , boolean , null , undefined , and symbol . When you make a strict comparison of two variables of primitive types that store it the same value, it turns out to be true . For example:

 const one = 1 const uno = 1 one === uno // true 

When PureComponent compares properties, it uses strict comparison. This works great for embedded primitive values ​​like <Toggler isOpen={true}/> .

The problem when comparing properties arises for other types, that is, sorry - the only type. Everything else in JS is Object . What about functions and arrays? In fact, all of these are objects. Let me quote an excerpt from the MDN documentation : “Functions are ordinary objects that have the additional ability to be called for execution.”

Well what can I say - JS is JS. In any case, a strict comparison of different objects, even if they contain the same values, returns false .

 const one = { n: 1 } const uno = { n: 1 } one === uno // false one === one // true 

So, if you embed an object in JSX code, an adequate property comparison in PureComponent will not be possible, resulting in a more time-consuming comparison of React elements. This comparison will only clarify that the component has not changed, as a result - the loss of time in the two comparisons.

 //   <Avatar user={{ id: 'ryan' }}/> //   <Avatar user={{ id: 'ryan' }}/> //   ,  - ,   {} !== {} //   () ,     

Since functions are objects, and PureComponent performs a strict check on the equality of properties, the comparison of built-in functions when analyzing properties always ends with the message that they are different, after which the transition to the comparison of elements will be made during the matching procedure.

You may notice that this applies not only to the built-in functions. The same can be said about ordinary objects, and about arrays.

In order for shouldComponentUpdate , when comparing identical functions, what we expect from it, it is necessary to preserve the referential identity of functions. For experienced JS developers, this is not such bad news. But, considering that Michael and I learned about 3,500 people after training, who have different levels of training, it can be noted that this task was not so easy for our students. It should be noted that the ES classes do not help here, so in this situation we have to use other JS features:

 class Dashboard extends Component { constructor(props) {   super(props)     //  bind?    ,     20,   //  .   //  ,    .   this.handleStuff = this.handleStuff.bind(this)   // _this -   .   var _this = this   this.handleStuff = function() {     _this.setState({})   }     //     ES, , ,       //   ( ,   babel    ).   //      ,       -     //     .   this.handleStuff = () => {     this.setState({})   } } //   ,      JavaScript, //       ,    TC39  //      . handleStuff = () => {} } 

It should be noted here that the study of methods of maintaining the reference identity of functions leads to surprisingly long conversations. I have no reason to call programmers for this, unless they want to fulfill the requirements of their eslint configuration. The main thing that I wanted to show is that the built-in functions do not interfere with optimization. And now I will share my own history of performance optimization.

How I worked with PureComponent


When I first learned about PureRenderMixin (this is a design from earlier versions of React, which later became PureComponent ), I used many measurements and evaluated the performance of my application. Then I added a PureRenderMixin to all components. When I took the measure of the performance of the optimized version, I hoped that as a result everything would be so wonderful that I could tell about it with pride.

However, to my great surprise, the application began to work more slowly.

Why? Let's think about it. If you have a Component , how many comparisons do you have to do when working with it? And what about PureComponent ? The answers, respectively, are as follows: "only one," and "at least one, and sometimes two." If the component usually changes during the update, PureComponent will perform two comparison operations instead of one (the properties and state in shouldComponentUpdate , and then the usual comparison of elements). This means that normally the PureComponen t will be slower, but sometimes faster. Obviously, most of my components were constantly changing, so, in general, the application began to work more slowly. Sadly

There is no universal answer to the question: "How to increase productivity?" The answer can only be found in the performance measurements of a particular application.

About three scenarios for using inline functions


At the beginning of the material, I showed three types of built-in functions. Now that a base has been prepared, let's talk about each of them. But please remember that PureComponent better to hold until you have measurements in order to evaluate the benefits of using this mechanism.

â–ŤDOM component event handler


 <button onClick={() => this.setState(…)} >click</button> 

Normally, inside event handlers for buttons, input fields, and other DOM components, nothing is done except calling setState . This usually makes the built-in functions the most pure approach. Instead of jumping around the file looking for event handlers, you can find them in the item description code. The React community usually welcomes this.

The button component (and any other DOM component) cannot even be a PureComponent , so there’s no need to worry about shouldComponentUpdate and the reference identity.

As a result, this can be considered slow only if one agrees that the simple definition of a function is a fairly large load on the system, which is worth worrying about. There is no evidence that this is so. Unreasonable disposal of the built-in event handlers is a familiar premature optimization.

â–Ť "Custom event" or "action"


 <Sidebar onToggle={(isOpen) => { this.setState({ sidebarIsOpen: isOpen }) }}/> 

If the Sidebar — PureComponent , we will not go PureComponent property comparison. Again, since the handler is simple, embedding it may be the best solution.

Now let's talk about events like onToggle , and why Sidebar compares them. There are only two reasons for finding differences in properties in shouldComponentUpdate :

  1. The property is used for rendering.
  2. The property is used to achieve a side effect in componentWillReceiveProps , in componentDidUpdate , or in componentWillUpdate .

Most properties of the form on<whatever> do not meet these requirements. Thus, most PureComponent use PureComponent lead to unnecessary comparisons, which forces developers to maintain, without need, the referential identity of the handlers.

It is necessary to compare only those properties that may change. Thus, handlers can be found in the item description code, and all this will work quickly, and if we are worried about performance, it can be noted that with this approach there will be less comparisons.

For most components, I would advise you to create a PureComponentMinusHandlers class and inherit from it, instead of inheriting from PureComponent . This will help to simply skip all feature checks. Just what you need. Well - almost what you need.

If you get a function and pass this function directly to another component, it will be obsolete. Take a look at this:

 // 1.    . // 2.     , //   ,   . // 3.    setState     // **  . // 4.     ,  //  . // 5.        //   ,     //   . class App extends React.Component { state = { val: "one" } componentDidMount() {   this.setState({ val: "two" }) } render() {   return <Form value={this.state.val} /> } } const Form = props => ( <Button   onClick={() => {     submit(props.value)   }} /> ) class Button extends React.Component { shouldComponentUpdate() {   // ,    ,  .   return false } handleClick = () => this.props.onClick() render() {   return (     <div>       <button onClick={this.props.onClick}>This one is stale</button>       <button onClick={() => this.props.onClick()}>This one works</button>       <button onClick={this.handleClick}>This one works too</button>     </div>   ) } } 

→ Here you can experiment with this code.

So, if you like the idea of ​​inheriting from PureRenderWithoutHandlers , do not pass your handlers, not participating in the comparison, directly to other components - they need to be wrapped in some way.

Now we either need to maintain a reference identity, or avoid a reference identity! Welcome to performance optimization. At a minimum, with this approach, the load falls on the optimized component, and not on the code that uses it.

I must honestly say that this example application is an addition to the material that I did after publishing with Andrew Clarke’s submission. So it may seem that I know exactly when to maintain referential integrity, and when - no.

Render render property


 <Route path="/topic/:id" render={({ match }) => (   <div>     <h1>{match.params.id}</h1>}   </div> ) /> 

The render — a template used to create a component that exists to create and maintain a shared state ( you can read more about this here ). The contents of the render property are unknown to the component. For example:

 const App = (props) => ( <div>   <h1>Welcome, {props.name}</h1>   <Route path="/" render={() => (     <div>       {/*         props.name    Route              ,  Route            PureComponent,              ,     .       */}       <h1>Hey, {props.name}, let's get started!</h1>     </div> )}/> </div> ) 

This means that the built-in function of the render property will not lead to problems with shouldComponentUpdate . , PureComponent .

, , render . — , .

Results


  1. Write the code as you are used to, realizing your ideas in it.
  2. Perform performance measurements to find bottlenecks. Here you can learn how to do it.
  3. Use PureComponent and shouldComponentUpdate only when necessary, skipping functions that are component properties (only if they are not used in life-cycle interceptor events to achieve side effects).

, , , . In order to think about their optimization, you need evidence to the contrary.

Dear readers! React-?

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


All Articles