React is known for providing a fast user experience by only updating the parts of the UI that have changed.
When looking into React's render performance, there are a few terms and concepts that can be hard to understand. It wasn't 100% clear to me what a VDOM was or how React decides to re-render components for quite some time.
In the first part of this article, I'll explain the most important concepts about rendering in React and how React decides to re-render a given component.
In the last section of this article, I'll show you what you can do to optimize the render performance of your React application.
If, after reading this, you have open questions or find an error, feel free to leave a comment or email me.
Rendering in React
What is rendering?
If we want to understand how React renders and re-renders work, it's a good idea to understand what happens behind the scenes of the library.
Rendering is a term that can be understood on different levels of abstraction. Depending on the context, it has a slightly different meaning. In any case, ultimately, it describes the process of generating an image.
To start off, we need to understand what the DOM (Document Object Model) is:
"The W3C Document Object Model (DOM) is a platform and language-neutral interface that allows programs and scripts to dynamically access and update the content, structure, and style of a document."
In plain English, this means that the DOM represents what you see on your screen when you open a website, expressed through the markup language HTML.
Browsers allow the JavaScript language to modify the DOM through an API: The globally available document
represents that state of the HTML DOM and provides us with functions to modify it.
You can modify the DOM with JavaScript through the DOM programming interface that contains
functions like document.write
, Node.appendChild
or Element.setAttribute
.
What is the VDOM?
Then we have the Virtual DOM (or VDOM) of React, another abstraction layer on top of that. It consists of your React application's elements.
State changes in your application will be applied to the VDOM first. If the new state of the VDOM requires a UI change, the ReactDOM library will efficiently do this by trying to update only what needs to be updated.
For example, if only the attribute of an element changes, React will only update the attribute of the HTML element by
calling document.setAttribute
(or something similar).
When the VDOM gets updated, React compares it to to a previous snapshot of the VDOM and then only updates what has changed in the real DOM. If nothing changed, the real DOM wouldn't be updated at all. This process of comparing the old VDOM with the new one is called diffing.
Real DOM updates are slow because they cause an actual re-draw of the UI. React makes this more efficient by updating the smallest amount possible in the real DOM.
Therefore we have to be aware of the difference between native and virtual DOM updates.
Read more about how this works in React's documentation about reconciliation.
What does this mean for performance?
When we talk about renders in React, we really talk about the execution of the render function, which doesn't always imply an update of the UI.
Let's see this in an example:
const App = () => {
const [message, setMessage] = React.useState('');
return (
<>
<Tile message={message} />
<Tile />
</>
);
};
When the state changes in the parent component (in this case, App
), the two Tile
components will re-render, even though
the second one doesn't even receive any props.
This translates to having the render
function being called three times, but actual DOM modifications only happen once in the Tile
component that displays the message:
The good news is that you don't have to worry too much about the performance bottlenecks of UI re-draws. React already optimizes this for you.
The bad news is: All those red dots on the left-hand side mean that the render function of these components has been executed.
The execution of these render functions has two drawbacks:
- React has to run its diffing algorithm on each of those components to check whether it should update the UI.
- All your code in these render functions or function components will be executed again.
The first point is arguably not that important since React manages to calculate the difference quite efficiently. The danger lies in the code that you wrote is being executed repeatedly on every React render.
In the example above, we have a small component tree. But imagine what happens if each node has more children, and these again might have child components. We'll see how we can optimize this.
Want to see re-rendering in action?
React DevTools lets you highlight renders under Components -> View Settings -> Highlight updates when components render. This will show you the virtual renders.
If you want to see native re-renders, you can do so in the Chrome DevTools, under the three-dot menu on the right -> More tools -> Rendering -> Paint flashing.
Now click through your application with first the React re-renders highlighted, and then the native renders, and you'll see how much React optimizes native rendering.
When does React re-render?
Above we saw what causes a re-draw of our UI, but what is calling React's render function to begin with?
React schedules a render every time the state of a component changes.
Scheduling a render means that this doesn't happen immediately. React will try to find the best moment for this.
Changing the state means that React triggers an update when we call the setState
function
(in React hooks, you would use useState
).
This doesn't only mean the component's render function will be called, but also that all its subsequent child components will re-render, regardless of whether their props have changed or not.
If your application is poorly structured, you might be running a lot more JavaScript than you expected because updating the
parent node implies running the render
function of all children.
In the last part of the article, we will see a few tips that help you to prevent this kind of overhead.
Why doesn't my React component update when its props change?
There are two common reasons why React might not update a component even though its props have changed:
- The props weren't updated correctly via
setState
- The reference to the prop stayed the same
As we already saw before, React re-renders a component when you call the setState
function to change the state (or the provided function from the useState
hook in function components).
As a result, the child components only update when the parent component's state changes with one of those functions.
Directly mutating the props object is not allowed since this won't trigger any changes, and React doesn't notice the changes.
this.props.user.name = 'Felix';
Instead of changing the props like this, you need to change the state in the parent component.
const Parent = () => {
const [user, setUser] = React.useState({ name: 'Felix' }); const handleInput = (e) => {
e.preventDefault();
setUser({ ...user, name: e.target.value, }); };
return (
<>
<input onChange={handleInput} value={user.name} />
<Child user={user} />
</>
);
};
const Child = ({ user }) => (
<h1>{user.name}</h1>
);
Note how I update the state using setUser
, which is the function I get from React.useState
. The equivalent to this in class components would be this.setState
.
Force a React component to rerender
In the two years that I've been working with React professionally, I've never come to a point where I needed to force a re-render. I encourage you to read the article from the beginning if that's what you're here for because usually there's a better way of dealing with React components that aren't updating.
However, if you absolutely need to force an update, you can do so with the following methods:
Using React's forceUpdate
function
This one is the most obvious one. In React class components, you can force a re-render by calling this function:
this.forceUpdate();
Force an update in React hooks
In React hooks, the forceUpdate
function isn't available. You can force an update without altering
the components state with React.useState
like this:
const [state, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
How to optimize re-renders
An example of inefficient re-renders is when a parent component controls the state of a child component. Remember: When the state of a component changes, all children will re-render.
I expanded the example I already used for explaining React.memo to have more nested children. Go ahead and try it out.
The numbers in yellow are counting the number of times the render
function of each component has been executed:
Paints
0
Paints
0
Paints
0
Paints
0
Even though we only updated the state of the blue component, a lot more renders of other components have been triggered.
Controlling when a component should update
React provides us with a few functions to prevent these unnecessary updates.
Let's have a look at them, after this, I'll show you another, more effective way of improving render performance.
React.memo
The first one, which I already gave away before, is React.memo
. I already wrote a more in-depth article on this, but
in summary, it's a function that prevents your React Hook components from rendering when the props don't change.
An example of this in action looks something like this:
const TileMemo = React.memo(({ children }) => { let updates = React.useRef(0);
return (
<div className="black-tile">
Memo
<Updates updates={updates.current++} />
{children}
</div>
);
});
There are a few more things you need to know about this before using it in production. I recommend you check out my article on React.memo after reading this one.
The equivalent for React classes is using React.PureComponent
.
shouldComponentUpdate
This function is one of React's lifecycle functions and allows us to optimize rendering performance by telling React when to update a class component.
Its arguments are the next props and the next state that the component is about to render:
shouldComponentUpdate(nextProps, nextState) {
// return true or false
}
This function is pretty easy to use:
Returning true
causes React to call the render function, returning false
prevents this.
Set the key attribute
In React, it is very common to do the following. Find out what's wrong with it:
<div>
{
events.map(event =>
<Event event={event} />
)
}
</div>
Here I forgot to set the key
attribute. Most linters will warn you about this, but
why is it so important?
In some cases, React relies on the key
attribute for identifying components and optimizing performance.
In the example above, if an event is being added to the beginning of the array, React will think that the first and all the subsequent elements have changed and will trigger a re-render of those. We can prevent this by adding a key to the element:
<div>
{
events.map(event =>
<Event event={event} key={event.id} /> )
}
</div>
Structure of your components
An even better way of improving re-renders is by restructuring your code a little bit.
Be careful where you place your logic. If you put everything in the root component of your application,
all the React.memo
functions in the world won't help you to fix your performance problems.
If you place it closer to where the data is used, chances are you don't even need React.memo
.
Check out the optimized version of the example and type in some text:
Paints
0
Paints
0
Paints
0
Paints
0
Paints
0
Paints
0
Paints
0
You see that even though the state updates, the other components don't re-render at all.
The only change I made was to move code that handles the state into a seperate component:
const InputSelfHandling = () => {
const [text, setText] = React.useState('');
return (
<input
value={text}
placeholder="Write something"
onChange={(e) => setText(e.target.value)}
/>
);
};
If you need to use the state in other parts of your application, you can do so by using React Context or alternatives like MobX and Redux.
In this article, I explain component composition more in detail and how it can help improve performance.
Conclusion
I hope I could give you a better understanding of how React's render mechanisms work and what you can do to get the most out of this. For this article, I had to do some additional research about the topic to get a better understanding of how React's render work.
I intend to write more about frontend performance, so if you want to get notified about the latest articles, follow me on Twitter and subscribe to my Email list.
If you came this far, you'll also want to check out my article about React.memo, which explains the API more in depth, some common pitfalls you could encounter, and why you shouldn't always use React.memo. Thanks for reading.