Premature optimization is the root of all evil.
For starters, it's worth remembering that the virtual DOM is already an optimization that allows React to run out of the box fast enough that you don't have to think about performance at all for a long time. For many projects, this is enough for a lifetime.
In general terms, React works like this:
Every time there's a change to the state of a component, a mechanism called "reconciliation" is triggered, which calculates the difference (diff) between the past state and the new state. From an algorithmic point of view, it's essentially a search for differences in the two trees. Generally, the algorithm that performs this calculation works with a complexity of O(n3).
If events are generated often, and the virtual tree has become large, lag will start to get very visible.
To solve this problem, React insists you use a key
attribute for all list items, which does not change for specific list items. This requirement makes it possible to optimize the performance of the algorithm by reducing the complexity to O(n).
The requirement for keys is checked by React itself. It will issue its own warnings in the browser console if it sees you're not using them.
In practice, rendering the entire application (virtual DOM) for any change is expensive. Imagine the application uses a text input box. This means that during typing, any click generates a virtual DOM in its entirety, from scratch. A good example is the Q&A on Hexlet, where we encountered this very problem. The forum has a rather large virtual tree, and its full rendering takes a certain amount of time.
The React Developer Tools extension has a special checkbox that lets you see the components that are rendered during events. Everything is displayed visually, i.e., after each event, the rendered components are surrounded with frames.
It's easy to see that for an application where nothing has been specifically done, everything will be rendered for every event. But events tend to change only a small part of the DOM. Entering text often doesn't change the DOM at all.
React avoids rerendering the components that haven't changed. In terms of conditions – you have to keep it clean, in other words, the component must essentially be a clean function.
Updating components triggers the following chain of functions:
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
You can stop rerendering by using shouldComponentUpdate()
. If this method returns false
, the component will not render at all. And since it's assumed that the component behaves as a pure function, it is sufficient to check inside this method that the props
and state
state haven't changed. It looks something like this:
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps)
|| !shallowEqual(this.state, nextState);
}
The shallowEqual()
function compares only the top level of objects. Otherwise this operation would have been too heavy. By the way, here you can see why you can't change the state directly: this.state.mydata.key = 'value'
. Since objects are compared by reference, changing an object will show that the object is the same, even though its contents have changed.
Since most components in typical applications do behave like pure functions, and the state is stored in a common root component, this technique can be applied everywhere, and React actively helps with this. So far you've only inherited classes from React.Component
, but you can also inherit from React.PureComponent
, in which shouldComponentUpdate
has been correctly implemented for you.
See the Pen js_react_performance_pure_component by Hexlet (@hexlet) on CodePen.
If you click the button, you can see that the root component is re-rendered, but the subcomponent is not.
But it's not that simple. It's very easy to break PureComponent
without realising..
The first issue awaits if you don't work with the default properties correctly:
class Table extends React.Component {
render() {
const { options } = this.props;
return (
<div>
{this.props.items.map(i =>
<Cell data={i} options={options || []} />
)}
</div>
);
}
}
Seemingly harmless code, but calling []
each time generates a new object (assuming options
is false
). This is easy to check: [] === []
will be false. In other ways, the data have not changed, but <Cell>
will be redrawn.
The takeaway is that you should use the built-in mechanism for default properties.
class App extends React.PureComponent {
render() {
return <MyInput
onChange={e => this.props.update(e.target.value)} />;
}
}
The problem in the code above is exactly the same: a new handler function (callback) is generated for each render
function call, which breaks effective updating. You already know the solution: define handlers as class-level properties.
Another interesting way to solve the problem of re-rendering an application is to use persistent data structures, specifically the immutable.js
library. This is a separate topic beyond the scope of this course.
The Hexlet support team or other students will answer you.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.
Programming courses for beginners and experienced developers. Start training for free
Our graduates work in companies:
From a novice to a developer. Get a job or your money back!
Sign up or sign in
Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.