Hello, this is Sahil Khokhar, and I work as a Frontend Architect at Mercari. This post is for Day 20 of Mercari Advent Calendar 2022.
Okay, let’s break down some technical details…
If you’re familiar with React, you probably know that it uses a virtual Document Object Model (or "DOM") to optimize updates to the actual DOM. This process of updating the DOM is also called DOM manipulation, and it’s at the heart of web development. The problem is that DOM manipulation can be really inefficient and slow. That’s where React comes in – it uses something called the Virtual DOM to make DOM manipulation more efficient and improve performance. When you update a React component’s state, the Virtual DOM compares the new version to the old version (kind of like taking two snapshots and finding the difference) and only makes the necessary changes to the actual DOM. This helps improve the overall performance of the application.
Now that we know a little bit about what’s going on under the hood in React, on the surface its top priority is to make sure the user interface stays "in sync" with the application state. To do this, React triggers re-renders for components as needed. It’ll do whatever it takes to make sure the UI stays up to date. In this blog post, I want to clear up some misconceptions about re-rendering and show you one of the simple ways to optimize the rendering performance of your react applications.
Understanding React Re-Renders
If you’ve struggled with understanding why a particular component re-renders multiple times or why renders are super slow, you are not alone! In fact, we have all spoken with numerous React developers who have struggled to fully grasp the underlying logic of re-rendering in their applications.
In an effort to clarify this concept, let us attempt to simplify it here…
Re-renders in react are triggered with a state change.
It’s the only way how re-renders are started. Let’s take a look at this with an example:
The above example demonstrates the use of a single tree with a component called
App. This component displays a button labeled "Increment" and shows the current count value below it. When the user clicks the "Increment" button, the count is incremented by 1.
In React, each component instance has a corresponding state. In the above example, the
App component has a single piece of state called
count. When the
count state changes (such as when the user clicks the "Increment" button), the App component will re-render to display the updated count. This process is a fundamental aspect of React’s functionality and is pretty straightforward.
In the event that our component hierarchy is more complex, this can introduce additional complexity.
In React, when a component re-renders, all of its descendant components are also re-rendered.
To illustrate this concept, let’s consider the following example:
In the above example, there are three components:
App is the top-level component and renders
Counter, which in turn renders
CountNumber. In terms of hierarchy,
CountNumber is the immediate descendant of
Counter, which is the immediate descendant of
Counter component has a state called
count associated with it. When the value of
count changes, the
Counter component will re-render and, since
CountNumber is a descendant of
Counter, it will also re-render.
It is a common misconception that a change in state within a descendant component will trigger a re-render of the parent component in React.
However, this is not the case.
React follows a one-way data flow, meaning that data can only flow downward through the component hierarchy and cannot flow upwards. Therefore, a change in state within a descendant component will not trigger a re-render of the parent component.
So, when the
count state changes in the above example, the
App component is not re-rendered.
The process of updating the virtual DOM to match the desired state of the UI in React is known as Reconciliation. This process occurs after a state change and involves calculating the difference, or "diff", between the virtual DOM and the desired state. The virtual DOM is then updated with the minimum number of operations necessary to bring it in line with the desired state. This helps to ensure that the React UI stays in-sync with state changes and helps to optimize the performance of the application by minimizing the number of DOM manipulations (remember we talked about React’s #1 priority in the beginning of the article).
Optimizing Rendering Performance
It is important to carefully consider the data flow through the component hierarchy and the proper association of state with component instances in React to avoid potential pitfalls. Incorrectly managing data flow and state association can lead to unintended re-renders and potentially negatively impact the performance of the application.
Now, let’s explore some examples of potential pitfalls in React rendering and how we can optimize rendering speed:
The above example consists of three components:
App component is the top-level component and renders the
Counter component, which in turn renders the
Imagine that the
ExpensiveComponent performs an expensive logical operation. This means that whenever the
Counter component is rendered or re-rendered, due to its association with the
ExpensiveComponent will also be re-rendered, even though it has no relation to the
It may be tempting to use
React.memo to memoize the
ExpensiveComponent and prevent it from re-rendering. While this can be a viable solution in some cases, it may not always be the most effective approach. Memoizing every component can be counter-productive if the component has a large number of props and few descendants, as the process of checking the props for changes may be slower than simply re-rendering the component. In such cases, making the
ExpensiveComponent pure with
React.memo may cause it to unnecessarily check its props on every render, leading to decreased performance.
So is there a way to optimize the performance of this code?
Yes, one option is to optimize the component hierarchy by moving the state down and properly abstracting it. If we closely examine the above example, we can see that only a specific portion of the
Counter component depends on the
count state. By extracting this code into a separate component and moving the
count state down into it, we can potentially improve performance.
Now, whenever re-renders are triggered for the
ExpensiveComponent is not re-rendered. Problem solved.
With the above smart code abstraction, it not only makes our code more readable but also improves the performance of our application (cherry on top for doing effective abstraction in your codebase).
What’s the moral?
The simple trick and concepts covered above have had a significant impact on the industry, such as reported by Brooks Lybrand:
The concept of effectively managing state and maintaining a proper component hierarchy is a fundamental aspect of writing high-quality code in React.
It is important to place state variables where they are used and to avoid premature optimization. As a general rule, it is advisable to write the code first, measure its performance, and only optimize if necessary. Simple techniques, such as above, that address these issues are often undervalued and deserve more attention. Rigid adherence to certain paradigms can result in micro-optimizations that may have only a minor impact on performance but significantly decrease code readability and organization.
Thank you for reading!
This article is motivated from blog-posts by Kent and Dan. Be sure to check those out too for more interesting insights.
Tomorrow’s article will be about the Kubernetes Scheduler by @sanposhiho. Look forward to it!