Front-end optimization reaction: virtual dom explained

Learn about react's virtual dom and use this knowledge to speed up your application. In this comprehensive primer on the insides of the framework, we'll demystify JSX, let you show how to react, explain how to find bottlenecks, and share some tips for avoiding common mistakes.

One of the reasons React has been rocking the front-end world and shows no sign of going down is its approachable learning curve: After you wrap your head around, the learning curve. n.JSX also has the whole " state vs. props " concept and you can go.

If you're already familiar with the way React works, you can skip straight to "Fixing things" .

But to really master your responses, you need to think about your responses . This article is to help you solve this problem. Take a look at the responses made to one of our projects :

A huge React table

A huge response sheet for ebay business .

With hundreds of dynamic, multi-layered rows, understanding the finer points of the framework is critical to keeping the user experience running smoothly.

You definitely feel it when it happens. Input fields will get laggy, checkboxes will be checked first, and modal verbs will show up when difficult.

In order to solve these problems, we need to cover the entire journey, a react component from definition to you define (and then update) on the page. fasten your seatbelt!

behind JSX

The front-end circle is known in the process as "transpiling", even though "compiling" would be a more correct term.

React developers urge you to use a combination of html and javascript called JSX when writing your components. However, browsers have no clue about JSX and its syntax. Browsers only understand simple javascript, so JSX has to be converted into it. Here is one divwhich has a class and some content:

<div className='cn'>
  Content!
</div>

The same code in "official" javascript is just a function call with several parameters:

React.createElement(
  'div',
  { className: 'cn' },
  'Content!'
);

Let's take a closer look at these arguments. The second is an element type . For html tags it will be a string with the tagname. The second parameter is an object that contains all the element properties . It can also be an empty object if there is no empty object. All arguments below are children of the element . The text in the element is also counted as child elements, so the string "content!" is placed as the third parameter of the function call.

You can already imagine what happens when we have more children:

<div className='cn'>
  Content 1!
  <br />
  Content 2!
</div>
React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',              // 1st child
  React.createElement('br'), // 2nd child
  'Content 2!'               // 3rd child
)

Our function now has five parameters: the element type, the attribute object, and three child elements. Since one of our children is also a well-known reaction, it will be depicted as a function call.

Now, we've covered two types of children: plain Stringor another phone React.createElement. However, other values ​​can also be used as arguments:

  • primitives falsenullundefinedandtrue
  • array
  • React components

Arrays are used to group and pass sub-elements as a parameter:

React.createElement(
  'div',
  { className: 'cn' },
  ['Content 1!', React.createElement('br'), 'Content 2!']
)

Of course, the power of react comes from the tags described in the html spec, but from user-created components such as:

function Table({ rows }) {
  return (
    <table>
      {rows.map(row => (
        <tr key={row.id}>
          <td>{row.title}</td>
        </tr>
      ))}
    </table>
  );
}

Components allow us to break templates into reusable blocks. In one example, the "function" component above accepts an array of objects with array row data, and returns a single React.createElementcall <table>element and its row as children.

Whenever we place a component into a layout like this:

<Table rows={rows} />

From a browser perspective, we wrote this article:

  React.createElement(Table, { rows: rows });

Note that this time our first parameter is not Stringdescribing an html element, but a reference to the function we defined when we coded our component. Our properties are now ours props.

Put the component on the page

So, we've converted all our JSX components to plain javascript, and now we have a series of function calls, among them other function calls, and other function calls... How do we convert them into dom elements that make up a web page?

For this we have a ReactDOMlibrary and its rendermethods:

function Table({ rows }) { /* ... */ } // defining a component

// rendering a component
ReactDOM.render(
  React.createElement(Table, { rows: rows }), // "creating" a component
  document.getElementById('#root') // inserting it on a page
);

When it ReactDOM.renderis called React.createElementlast called, it returns the following objects:

// There are more fields, but these are most important to us
{
  type: Table,
  props: {
    rows: rows
  },
  // ...
}

These objects make up the meaning of the virtual dom in reaction.

They will be compared to each other in all further renderings and finally converted to real dom (vs virtual ).

Here's another example: this time using diva class attribute and several sub-attributes:

React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',
  'Content 2!',
);

become:

{
  type: 'div',
  props: {
    className: 'cn',
    children: [
      'Content 1!',
      'Content 2!'
    ]
  }
}

Note that in the past the individual parameter functions were used to find their in-position keys React.createElementunder a/s . So it doesn't matter if the children are passed as arrays or parameter lists - in the resulting virtual dom object, they will all end up together eventually.childrenprops

Also, we could add the child directly to the code and the result would still be the same:

<div className='cn' children={['Content 1!', 'Content 2!']} />

Once the dummy dom object is constructed, ReactDOM.renderan attempt will be made to convert it into a Dom node that our browser can display according to these rules:

  • If the typeattribute holds the chord use the tag name - creates a tag that lists all the attributes listed below props.

  • If we have a function or class type- call it and recursively repeat a result.

  • If anything goes childrendown props- repeat the process one by one and place the result inside the parent's Dom node.

So we get the following html (for our table example):

<table>
  <tr>
    <td>Title</td>
  </tr>
  ...
</table>

rebuilt in

In practice, it renderis usually called once on the root element, and further updates are passed through state.

Note: "re" is in the title! When we wanted to do, the real reaction started. Updating a page didn't replace everything. There is no way how we can do this. Let's start with the simplest one call again forReactDOM.render the same node .

// Second call
ReactDOM.render(
  React.createElement(Table, { rows: rows }),
  document.getElementById('#root')
);

This time, the code above will be different from what we have already seen. The Response will create all Dom nodes from scratch and put them on the page, and the Response will start a reconciliation (or "diffing") algorithm to determine which parts of the node tree must be updated and can remain untouched.

So, how does it work? Just a few simple scenarios and understanding them our optimizations will help us a lot. Remember that we are now looking at objects that are representations of nodes in the response virtual dom.

  • Scenario 1: typeIt is a string that typemaintains the same distance on the phone and propsdoes not change.
// before update
{ type: 'div', props: { className: 'cn' } }

// after update
{ type: 'div', props: { className: 'cn' } }

This is the simplest example: the dom remains the same.

  • Assumption 2: typeStill the same string, propsdifferent.
// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'div', props: { className: 'cnn' } }

As we typestill mean that the html element responds by knowing how to change its attributes via standard Dom calls without removing the node from a kind of tree.

  • Scenario 3: typeHas changed from Stringor Stringto the component.
// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'span', props: { className: 'cn' } }

When the response now sees a different type, it won't even try to update our node: the old element will be removed ( downloadedalong with all the children . So it can be very expensive to replace one element for an element of a completely different high-level dom tree. Fortunately, this rarely happens in the real world.

It's important to remember that react uses ===(triple equals) comparison typevalues, so they must be the same instance of the same class or the same function.

The next scene is more interesting because that's how we often use reactions.

  • Scenario 4: typeis a component.
// before update:
{ type: Table, props: { rows: rows } }

// after update:
{ type: Table, props: { rows: rows } }

"But nothing has changed!" you might say, and you would be wrong.

Note that the component's render(only class components have this method explicitly defined), but with ReactDOM.render. The word "rendering" is indeed overused in the reactive world.

If it type's a reference to a function or class (i.e. a regex react component) and we start the tree adjustment process, then react always tries to look inside the component to make sure the value returned renderhasn't changed (a precaution against side effects). Flush and repeat every component down the tree - yes, complex rendering can get expensive too!

take care of children

In addition to the four common scenarios described above, we also need to consider the responsive behavior when an element has multiple children. Let's assume we have an element like this:

// ...
props: {
  children: [
      { type: 'div' },
      { type: 'span' },
      { type: 'br' }
  ]
},
// ...

We want to shuffle these kids:

// ...
props: {
  children: [
    { type: 'span' },
    { type: 'div' },
    { type: 'br' }
  ]
},
// ...

and then?

If "diffing", the react sees any internal arrays props.childrenit starts comparing the elements in it with the elements in the arrays it saw before, and then looks at them in turn: index 0 will do with index 0, index 1, and index 1, etc. Compare. For each pair, the reaction will apply the set of rules described above. In our case it sees divbecoming a spanso Scenario 3 will be applied. This is not very efficient: imagine that we have deleted the first row from a table of 1000 rows. React will have to "update" the remaining 999 children as their content will now not be equal if compared to the previous rep index.

Luckily, react has built -ins to solve this problem. If the element has an keyattribute will compare the value of the element. keyNot by index. As long as the keys are unique, React will move around the elements without removing them from the Dom tree and putting them back (the process known in React is mount/unmount ).

// ...
props: {
  children: [ // Now React will look on key, not index
    { type: 'div', key: 'div' },
    { type: 'span', key: 'span' },
    { type: 'br', key: 'bt' }
  ]
},
// ...

when the state changes

We didn't touch propsthe part of reactive philosophy until now, but ignored it state. Here's a simple "stateful" component:

class App extends Component {
  state = { counter: 0 }

  increment = () => this.setState({
    counter: this.state.counter + 1,
  })

  render = () => (<button onClick={this.increment}>
    {'Counter: ' + this.state.counter}
  </button>)
}

So, we have a counterkey in our state object. When the button is clicked it increments its value and changes the button text. But what happens in a Dom when we do that? Which parts of it will be recalculated and updated?

The call this.setStatealso causes a re-render, but not the entire page, but just a component itself and its children . Parents and siblings are free. This is handy when we have a large tree and we only want to redraw part of it.

pinning problem

We've prepared small demo apps so you can see the most common problems in the wild before we go to fix them. You can view its source code. here . You also need the react dev tools so make sure you have them installed for your browser.

The first thing we'll look at is which elements and when will cause the virtual dom to be updated. Navigate to the responsive panel in your browser's dev tools and select the "Highlight Updates" checkbox:

React DevTools in Chrome with 'Highlight updates' checkbox selected

Respond with "Highlight Updates" checkbox in chrome

Now try adding a row to the table. As you can see, borders appear around every element on the page. This means that every time a row is added, the response is to compute and compare the entire virtual dom tree. Now try hitting a counter button in a row. You can see how the virtual dom is updated when it changes state- only the relevant factor and its children are affected.

React to where the problem might be, but tell us the details: especially updating the problem means "diffing" elements or mounting/resetting them. To find out more information, we need to use react's built-in profiler (note that it won't work in production mode).

Add ?react_perfany url for your app and go to the "performance" tab in chrome browser. Hit the record button and tap around the table. Add some lines, change some counters, and hit Stop.

React DevTools' 'performance' tab

React DevTools 'Performance' tab

In the resulting output, we are interested in "user timings". Zoom to the timeline until you see the Reaction Tree Coordination group and its children. These are the names of our components [latest] or [mountain] next to them.

Most of our performance issues fall into these two categories.

Either the component (and all branches from it) is being reinstalled on every update for some reason, we don't want it (slow reinstallation), or we're doing expensive reconciliations, large branches, despite not having any Change.

Fix things: install/install

Now, when we found out a bit about how react decided to update the virtual dom and learned how to see what's going on behind the scenes, we're finally ready to fix things! First, let's deal with mounts/unmounts.

If you just consider that multiple children of any element/component are represented as inside the array .

Consider this:

<div>
  <Message />
  <Table />
  <Footer />
</div>

In our virtual dom it would be represented as:

// ...
props: {
  children: [
    { type: Message },
    { type: Table },
    { type: Footer }
  ]
}
// ...

We have a simple Messageone that divholds some text (think notices about your garden variety) and a huge Tablespan of, let's say, 1000+ lines. They are both surrounded children divso they are placed below props.childrenthe parent node and they don't happen to have a key. And react doesn't remind us to assign the key via console warning because the child element is being passed to the parent React.createElementas a parameter list, not an array.

Now our user has dismissed the notification, Messageremoved from the tree. Tableand Footerthe rest are.

// ...
props: {
  children: [
    { type: Table },
    { type: Footer }
  ]
}
// ...

How does the reaction look? It sees it as an array of children that change shape: children[0]hold Messagenow it occupies Table. There is no key to compare to, so compare typesince they both refer to the function (as well as the heterogeneous function) n.unmounts the whole Tableand then mounts it, rendering all the children: 1000+ lines!

So, you can add unique keys (but using keys is not the best option in this particular example), or go for a smarter trick: use short-circuiting boolean estimates which are a feature of javascript and many other modern languages. Look:

// Using a boolean trick
<div>
  {isShown && <Message />}
  <Table />
  <Footer />
</div>

Even Messageout of the screen, the props.childrenparent divwill still hold the 3 element, with children[0]value false(boolean). Remember true/ falsenulland are all allowed value properties undefinedof virtual dom objects ? typeWe finally came to this conclusion:

// ...
props: {
  children: [
    false, //  isShown && <Message /> evaluates to false
    { type: Table },
    { type: Footer }
  ]
}
// ...

So, Messageor not, our indices won't change, and of Tablecourse, will still reconcile with Table(referring to the component's reference) typestarting anyway, but just comparing virtual doms is faster than deleting Dom nodes and creating them from scratch. .

Now let's look at something more evolved. We know you like ad hoc s. A higher-order component is a function that takes the component as a parameter, does something, and returns a different function:

function withName(SomeComponent) {
  // Computing name, possibly expensive...
  return function(props) {
    return <SomeComponent {...props} name={name} />;
  }
}

This is a very common pattern, but you need to handle it with care. consider:

class App extends React.Component() {
  render() {
    // Creates a new instance on each render
    const ComponentWithName = withName(SomeComponent);
    return <SomeComponentWithName />;
  }
}

We are creating a special rendermethod inside a parent. When we re-render the tree, our virtual dom looks like this:

// On first render:
{
  type: ComponentWithName,
  props: {},
}

// On second render:
{
  type: ComponentWithName, // Same name, but different instance
  props: {},
}

Now, the response will like to just run an algorithm based on the above ComponentWithNamebut, as this time, the same name refers to a different instance of the triple equality comparison fails, rather than a reconciliation, a full reinstallation must occur. Note that it also causes states to lose as described here . Fortunately, it's easy to fix: you need to always be on render:

// Creates a new instance just once
const ComponentWithName = withName(Component);

class App extends React.Component() {
  render() {
    return <ComponentWithName />;
  }
}

Fix things: update

So, now we make sure not to reinstall stuff unless necessary. However, any change to a component located near the root of the tree results in the reconnection and reconciliation of all subtrees. The structure is complex, expensive, and often avoidable.

Would be great to have a way to tell React not to look at a certain branch, as we are confident there were no changes in it.

This approach exists and it involves something called shouldComponentUpdateit's component lifecycle . This method is called on every previous call to the component renderand receives new values ​​for props and state. We are then free to compare them to the current value and decide if the component should be updated (returned). trueOr falsethat is. If we return falsereact doesn't re-render the component involved and doesn't look at its children.

Comparing the two groups is usually sufficient propsas well as statea simple shallow comparison: if the values ​​at the top level are different, we don't have to update. Shallow comparisons are not a feature of javascript, but there are a lot of utilities for that.

With their help, we can write our code as follows:

class TableRow extends React.Component {

  // will return true if new props/state are different from old ones
  shouldComponentUpdate(nextProps, nextState) {
    const { props, state } = this;
    return !shallowequal(props, nextProps)
           && !shallowequal(state, nextState);
  }

  render() { /* ... */ }
}

But you don't even have to code it yourself, because the response includes this feature, the class call React.PureComponent . It's like React.Componentonly a shallow prop/state comparison shouldComponentUpdatehas been implemented for you .

Sounds like a no-brainer, just swap Componentit for PureComponentbeing extendspart of your class to define and enjoy efficiency. But don't be so fast! Consider these examples:

<Table
    // map returns a new instance of array so shallow comparison will fail
    rows={rows.map(/* ... */)}
    // object literal is always "different" from predecessor
    style={ { color: 'red' } }
    // arrow function is a new unnamed thing in the scope, so there will always be a full diffing
    onUpdate={() => { /* ... */ }}
/>

The code snippet above demonstrates the three most common code anti-patterns. Try to avoid them!

If you take care to create all objects, arrays and functions out of renderdefinitions and make sure they don't change between calls - you are safe.

You can observe that PureComponentin the update demo all table positions Rows are "cleaned". If you turn on "Highlight Updates" in Responsive DevTools, you will notice that only the table itself and the new rows are being rendered in row inserts, all other rows remain unchanged.

However, if you can't wait to go all out on pure components and implement them everywhere in your application - stop yourself. Comparing the two groups propsand statenot being free is not worth it for most basic components: more time to run. shallowComparethan in the algorithm.

Use this rule of thumb: pure components are fine for complex forms and tables, but they often simplify simple elements like buttons or icons.

Follow Xiaobian for more exciting content

You can also join our front-end learning qun: listen to quality free learning classes every day

At the same time, I will share the high-quality information for you, 2-1-3-1-2-6-4-8-6 Invitation code: Fallen Leaves

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324912690&siteId=291194637