React has always been popular in the front-end world, and it’s not that hard to learn. Just learn JSX, understand State and Props, and have fun with it. However, to become an expert on React, you need to have a deeper understanding of React.

This is a front page for Choerodon

In complex front-end projects where a page can contain hundreds of states, a more nuanced understanding of the React framework is important for front-end optimization. There was a time when clicking on a record to display details on this page would stall for seconds, and that was just the result of front-end rendering.

To be able to address these issues, developers need to understand the React component from its definition to its rendering (and then updating) on the page.

React uses a syntax (called JSX) that mixes HTML and JavaScript when writing components. However, JSX and its syntax are unknown to browsers, who can only understand pure JavaScript, so they must convert JSX to HTML. Here is the JSX code for a div with a class and a few things:

<div className='cn'>The text</div>
Copy the code

React converts JSX to normal js, which is a function call with many arguments:

React.createElement(
  'div',
  { className: 'cn' },
  'text'
);
Copy the code

Its first parameter is a string, the corresponding HTML tag name, the second argument is that it all of the attributes of objects, of course, it may also be the object is empty, the rest of the parameters are all child elements under the element, the text also will be treated as a child, so the third parameter is the “text”.

By now you should be able to imagine what happens when you have more children in this element.

<div className='cn'>Text 1<br />Text 2</div>
Copy the code
React.createElement(
  'div',
  { className: 'cn' },
  'text 1'.// 1st child
  React.createElement('br'), // 2nd child
  'text 1'               // 3rd child
)
Copy the code

The current function takes five arguments: the type of the element, the object of all the attributes, and three child elements. Since a child is also an HTML tag known to React, it will also be interpreted as a function call.

So far, this article has looked at two types of child arguments, one for plain string text and one for calling other react.createElement functions. In fact, other values can also be used as parameters, such as:

  • Basic types false, null, undefined, and true
  • An array of
  • The React components

Arrays are used because sub-components can be grouped and passed as a parameter:

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

Of course, the power of React comes not from the tags described in the HTML specification, 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>
  );
}
Copy the code

Components allow developers to decompose templates into reusable chunks. In the example of the “pure function” component above, the component takes an array of objects containing table row data and returns the react.createElement pair

Whenever a developer puts a component into a JSX layout it looks like this:

<Table rows={rows} />
Copy the code

But from the browser’s perspective, it looks like this:

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

Notice that the first argument this time is not an HTML element described as a string, but a reference to the component (that is, the function name). The second argument is the props object passed to the component.

Place the component on the page

Now that the browser has converted all JSX components to pure JavaScript, the browser now gets a bunch of function calls with other function calls as arguments, and other function calls…… How do I convert them into the DOM elements that make up a web page?

To do this, developers need to use the ReactDOM library and its Render method:

function Table({ rows }) { / *... * / } // Component definition

// Render a component
ReactDOM.render(
  React.createElement(Table, { rows: rows }), // "create" a Component
  document.getElementById('#root') // Put it in the DOM
);
Copy the code

When reactdom.render is called, react.createElement is eventually called, which returns the following objects:

// There are many other fields in this object, but these are the most important for developers right now.
{
  type: Table,
  props: {
    rows: rows
  },
  // ...
}
Copy the code

These objects constitute the Virtual DOM in the React sense

They will be compared to each other in all further rendering and will eventually be converted to the real DOM (compared to the Virtual DOM).

Here’s another example: this time a div has a class attribute and several child nodes:

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

To:

{
  type: 'div'.props: {
    className: 'cn'.children: [
      'Content 1! '.'Content 2! ']}}Copy the code

All the arguments passed to the React. CreateElement function, except for the first and second arguments, are in the children property of the props object. No matter what function is passed in, they are eventually passed as children in the props object.

Furthermore, developers can add the children attribute directly to the JSX code, place the children directly in children, and the result will still be the same:

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

After the Virtual DOM object is created, reactdom.render attempts to translate it into a DOM node that the browser can understand by following these rules:

  • ifThe type attribute in the Virtual DOM object is a tag name of type string,itCreate a tag that contains all the properties in props.
  • ifThe type attribute in a Virtual DOM object is a function or class,itIf I call it, it’ll probably return a Virtual DOM and then I’ll recursively call the process.
  • ifProps has the children property,itDo the above for each element in children and place the returned result into the parent DOM node.

Finally, the browser gets the following HTML (for the table example above) :

<table>
  <tr>
    <td>Title</td>
  </tr>.</table>
Copy the code

Reconstruction of the DOM

The next step is for the browser to “rebuild” a DOM node. If the browser updates a page, obviously, developers don’t want to replace all elements of the page, and this is where React’s real magic comes in. How can it be done? Start with the simplest method and re-call the reactdom.render method on this node.

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

This time, the code execution logic above will be different from the code you see. Instead of creating all the DOM nodes from scratch and putting them on the page, React will use the “diff” algorithm to determine which parts of the node tree must be updated and which can remain unchanged.

So how does it work? There are only a few simple cases that understanding them will be of great help in optimizing the React program. Remember that the objects you see next are used to represent nodes in the React Virtual DOM.

▌Case 1: Type is a string. Type remains the same between calls, and props does not change.

// before update
{ type: 'div'.props: { className: 'cn'}}// after update
{ type: 'div'.props: { className: 'cn'}}Copy the code

This is the simplest case: the DOM stays the same.

▌Case 2: Type is still the same string, but props is different.

// before update:
{ type: 'div'.props: { className: 'cn'}}// after update:
{ type: 'div'.props: { className: 'cnn'}}Copy the code

Because Type still represents an HTML element, React knows how to change its attributes through standard DOM API calls without removing nodes from the DOM tree.

▌Case 3: Type has been changed to a different component String or changed from a String component to a component.

// before update:
{ type: 'div'.props: { className: 'cn'}}// after update:
{ type: 'span'.props: { className: 'cn'}}Copy the code

Because React now sees a different type, it won’t even try to update the DOM node: the old element will be removed (unmounted) along with all its children. Therefore, replacing completely different elements in the DOM tree can be very expensive. Fortunately, this rarely happens in practice.

It’s important to remember that React uses === (third order) to compare type values, so they must be the same class or the same instance of the same function.

The next scenario is even more interesting, because this is how developers most often use React.

▌Case 4: Type is a component.

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

// after update:
{ type: Table, props: { rows: rows } }
Copy the code

You may say, “This doesn’t seem to have changed at all,” but that’s not true.

If Type is a reference to a function or class (that is, the regular React component) and the tree diff comparison process is initiated, React will always try to see all children inside the component to ensure that the return value of Render has not changed. Even comparing each component under the tree – yes, complex rendering can also become expensive!

Children in component

In addition to the four common scenarios described above, developers also need to consider React behavior when an element has multiple child elements. Suppose we have an element like this:

// ...
props: {
  children: [{type: 'div' },
      { type: 'span' },
      { type: 'br'}},// ...
Copy the code

The developers wanted to rerender it like this (span and div swapped places) :

// ...
props: {
  children: [{type: 'span' },
    { type: 'div' },
    { type: 'br'}},// ...
Copy the code

So what happens?

When React sees props. Children in any array type, it will start comparing the elements in it to the elements in the array it saw earlier in order: Index 0 will be compared to index 0 and index 1 to index 1. React will apply the above rule set to each pair of child elements for comparison updates. In the example above, it saw a div become a span in scenario 3. But there’s a problem: suppose the developer wants to remove the first row from the 1000 row table. React had to “update” the remaining 999 children because their content would now be unequal if compared to the previous index by index representation.

Fortunately, React has a built-in way to solve this problem. If the element has a key attribute, the element is compared by key rather than index. As long as the key is unique, React moves elements without removing them from the DOM tree and then puts them back in (a process called mount/unload in React).

// ...
props: {
  children: [ // React now compares by key instead of index
    { type: 'div'.key: 'div' },
    { type: 'span'.key: 'span' },
    { type: 'br'.key: 'bt'}},// ...
Copy the code

When the state changes

So far this article has only touched on parts of the props, React philosophy, but has ignored state. Here is a simple “stateful” component:

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

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

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

Now, the state object in the above example has a counter property. Clicking the button increases its value and changes the button text. But what happens to the DOM when the user clicks? Which part of it will be recalculated and updated?

Calling this.setState also causes rerendering, but not the entire page, only the component itself and its children. Parents and siblings can be spared.

To fix the problem

This article has prepared a DEMO that looks like this before fixing the problem. You can view the source code here. But before you do that, you’ll need to install React Developer Tools.

The first thing to look at when you open the demo is which elements and when they cause Virtual DOM updates. Navigate to the React panel in your browser’s Dev Tools, click Settings and select the “Highlight Updates” check box:

Now try adding a row to the table. As you can see, every element on the page has a border around it. This means that React evaluates and compares the entire Virtual DOM tree each time a row is added. Now try pressing the counter button in a row. You will see how the Virtual DOM is updated (state is updated only for related elements and their children).

React DevTools hints at where the problem might be, but doesn’t give the developers any details: specifically if the update in question was different after the element “diff”, or if the component was unmounted/mounted. To learn more, developers need to use React’s built-in profiler (note that it doesn’t work in production mode).

Go to the “Performance” TAB in Chrome DevTools. Click the Record button, then click the form. Add some rows, change some counters, and click the “Stop” button. After a while developers will see:

In the result output, developers need to pay attention to “Timing”. Scale the timeline until you see the React Tree Reconciliation group and its children. These are component names, with [update] or [mount] next to them. You can see that one TableRow is being mounted and all the other TableRow are being updated, which is not what the developers wanted.

Most performance problems are caused by [update] or [mount]

A component (and everything under it) remounts for some reason with every update, and the developer doesn’t want it to happen (remounts are slow), or to perform an expensive redraw on a large branch, even though nothing seems to have changed.

Repair the mount/unmount to

Now, when developers understand how React decided to update the Virtual DOM and what’s going on behind the scenes, they’re finally ready to solve the problem! Fixing performance problems starts with fixing mount/unmount.

If the developer internally represents multiple children of any element/component as arrays, the program can get a significant speed boost.

Consider:

<div>
  <Message />
  <Table />
  <Footer />
</div>
Copy the code

In the virtual DOM, will be expressed as:

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

A simple Message component (which is a div with some text, like the top notification of a toothfish) and a very long Table, say 1000 + lines. They are both children of the div element, so they are placed under props. Children of the parent node, and they have no key. React doesn’t even use console warnings to remind developers to allocate keys, because the child node React. CreateElement is passed to the parent node as a parameter list rather than an array.

Now the user has turned off the top notification, so Message is removed from the tree. Table and Footer are the remaining children.

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

How does React view it? It sees it as a series of child: children[0] types that have changed type. The type used to be Message, but now it is Table. Since they are all references to functions (and different functions), it unloads the entire Table and installs it again, rendering all its descendants: 1000 + lines!

So you can add a unique key (but using key is not the best choice in this particular case) or use smarter trick: Boolean short-circuitry using &&, which is a feature of JavaScript and many other modern languages. Like this:

<div>
  {isShowMessage && <Message />}
  <Table />
  <Footer />
</div>
Copy the code

Even if Message is turned off (no longer displayed), the props. Children parent div will still have three elements, and children[0] will have a value of false (Boolean). Remember that true/false, null, and even undefined are all allowed values for the type attribute of the Virtual DOM object? Browsers end up with something like this:

// ...
props: {
  children: [
    false.// isShowMessage && 
       short-circuited to false
    { type: Table },
    { type: Footer }
  ]
}
// ...
Copy the code

So, the index doesn’t change whether the Message is displayed or not, and the Table is still compared to the Table, but simply comparing the Virtual DOM is usually much faster than deleting DOM nodes and creating them from it.

Now let’s move on to something more advanced. Developers love HOC. A higher-order component is a function that takes a component as an argument, adds some behavior, and returns a different component (function) :

function withName(SomeComponent) {
  return function(props) {
    return <SomeComponent {. props} name={name} />; }}Copy the code

The developer creates HOC in the parent Render method. When React needs to re-render the tree, the React Virtual DOM will look like this:

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

React now only runs a diff algorithm on ComponentWithName, but this time it references a different instance with the same name, so the comparison fails and must be completely remounted. Note that it also causes state loss, which fortunately is easy to fix: just return the same instance:

/ / the singleton
const ComponentWithName = withName(Component);

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

Repair the update

Now the browser has ensured that it won’t reload anything unless necessary. However, any changes made to a component located near the DOM root directory will result in all of its children being weighed. Complex, expensive and often avoidable.

It would be nice if there was a way to tell React not to look at a branch because it doesn’t change anything.

This approach exists and involves a component lifecycle function called shouldComponentUpdate. React calls this method before each component is called and receives new values for props and state. The developer is then free to compare the difference between the new value and the old value and decide if the component should be updated (returning true or false). If the function returns false, React will not re-render the offending component or view its children.

A simple, shallow comparison between two sets of props and state is usually sufficient: if the values of the top properties are the same, the browser doesn’t have to update. Shallow comparisons are not a feature of JavaScript, but there are many ways for developers to implement them themselves, or to use methods written by others in order not to reinvent the wheel.

After introducing the shallow comparison NPM package, developers can write the following code:

class TableRow extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    const { props, state } = this;
    return! shallowequal(props, nextProps) && ! shallowequal(state, nextState); } render() {/ *... * /}}Copy the code

But you don’t even have to write your own code, because the React in a group called the React. PureComponent embedded in the class the function, it is similar to React.Com ponent, But shouldComponentUpdate already implements shallow props/state comparisons for you.

You might be tempted to replace Component with PureComponent if you can. But developers can also have rerendering problems if they use PureComponent incorrectly. Consider the following three scenarios:

<Table
    // Map returns a new array instance each time, so each comparison is different
    rows={rows.map(/ *... * /)}
    // Each passing object is a new object, and the reference is different.
    style={ { color: 'red'}}// The same goes for the arrow function, which has a different reference each time.
    onUpdate={() => { / *... * /}} / >Copy the code

The code snippet above demonstrates three of the most common anti-patterns, so avoid them!

Using PureComponent correctly, you can see here how all TableRow’s rendered are “purified”.

However, if you can’t wait to use purely functional components, that’s a mistake. Comparing two sets of props and state is not free, or even worthwhile for most basic components: running shallowCompare takes more time than the Diff algorithm.

Use this rule of thumb: Pure components are good for complex forms and tables, but they often slow down simple elements like buttons or ICONS.

Now that you’re familiar with the React rendering mode, it’s time to start your front-end optimization journey.

About the Choerodon toothfish

Choerodon is an open source enterprise services platform, based on Kubernetes’ container orchestration and management capabilities, integrating DevOps toolchains, microservices and mobile application frameworks to help enterprises achieve agile application delivery and automated operations management. It also provides IoT, payment, data, intelligent insights, enterprise application marketplace and other business components to help enterprises focus on their business and accelerate digital transformation.

You can also learn about the latest developments of toothfish, product features and participate in community contributions through the following community channels:

  • Liverpoolfc.tv: choerodon. IO
  • BBS: forum. Choerodon. IO
  • Github:github.com/choerodon
  • Choerodon Toothfish on wechat
  • The Choerodon toothfish