In this two-part article, we will take a look at common mistakes that hurt the performance of a React app badly.
But before taking a look at these mistakes and how to solve them, we need to understand the React internals, more precisely we need to understand how JSX and the reconciliation algorithm work in React. This will be the subject of part 1 of this article; so let’s dive in.
Let’s take a closer look at these arguments. The first one is a type of element. For HTML tags it will be a string with a tag’s name. A second argument is an object with all of the element’s attributes. It can also be an empty object if there are none. All the following arguments are element’s children. Text inside an element also counts as a child, so a string ‘Hello World!’ is placed as the third argument to a function call.
And an element with multiple children gets compiled as follows :
So the types of a React child can be a string, a React.createElement call, primitives (false, null, undefined, and true), arrays, and other React components.
Because the power of React resides in the fact that you can write your own Custom reusable components, most of the time you will be writing something like this in your JSX:
And this will get compiled to :
Note that this time our first argument is not a String describing an HTML element, but a reference to a function that we defined when we coded our component. Our attributes are now our props.
Rendering a component
This is where the ReactDOM library and its render method comes into play.
When the render method gets called, it returns the following object. These objects constitute Virtual DOM in React’s sense.
Note: These objects will be compared to each other on all further renders and eventually translated into a real DOM (as opposed to virtual).
After a Virtual DOM object is built, ReactDOM.render will try to transform it into a DOM node our browser can display according to these rules:
- If the type is a String with a tag name: create a tag with all attributes listed under props.
- If the type is a function or a class: call it and repeat the process recursively on a result.
- If there are any children under props: repeat the process for each child one by one and place results inside the parent’s DOM node.
The re-render phase
Note that “re” in the heading! The real magic in React starts when we want to update a page without replacing everything.
So React schedules a render every time the state or props of a component changes (in other words when the setState() or forceUpdate() methods are called). Note only the component itself and its children gets re-rendered. Parents and siblings are spared.
When a re-render is triggered, instead of creating all DOM nodes from scratch and putting them on the page, React will start the reconciliation (or “diffing”) algorithm to determine which parts of the node tree have to be updated and which can be left untouched.
You might be wondering how does this works. There is a handful of scenarios to understand. Keep in mind that we are now looking at objects that serve as a representation of a node in the React Virtual DOM.
- Scenario 1: type is a string (simple HTML tag), type stayed the same across calls, props did not change either.
This is the simplest case DOM stays the same.
- Scenario 2: type is still the same string, props are different.
As our type still represents an HTML element, React knows how to change its properties through standard DOM API calls, without removing the node from a DOM tree.
- Scenario 3: type has changed to a different String, or from String to a component.
As React now sees that the type is different, it would not even try to update our node; old element will be removed (unmounted) together with all its children. Thus, replacing an element for something entirely different high up the DOM tree can be quite expensive.
It is important to remember that React uses === (triple equals) to compare type values, so they have to be the same instances of the same class or the same function.
- Scenario 4: type is a component.
If type is a reference to a function or a class (that is, your regular React component), and we started tree reconciliation process, then React will always try to look inside the component to make sure that the values returned on render did not change (sort of a precaution against side-effects). Rinse and repeat for each component down the tree.
Besides these 4 common cases, we also need to account for React’s behavior when an element has more than one child. Let’s say we have such an element, and we want to shuffle those children around:
What happens in this case?
If, while “diffing”, React sees any array inside props.children, it starts comparing elements in it with the ones in the array it saw before by looking at them in order: index 0 will be compared to index 0, index 1 to index 1, etc. For each pair, React will apply the set of rules described above. In our case, it sees that div became an h1 so Scenario 3 will be applied. That is not very efficient: imagine that we have changed the first row from a 1000 list of children. React will have to “update” remaining 999 children, as their content will now not be equal if compared to previous representation index-by-index.
Luckily, React has a built-in solution to this problem. If an element has a key property, elements will be compared by a value of a key, not by index. As long as keys are unique, React will move elements around without removing them from DOM tree and then putting them back (a process known in React as mounting/unmounting).
In part 2 we will use this knowledge to analyze the most common mistakes that cause unnecessary re-renders and re-mounts and of course we will try to solve them.