React background
Before I dive into how to tune react for performance, I want to give a quick overview of how React/JSX works behind the scenes. This overview is going to be a light brush on the very, very basic idea behind the framework, so please jump ahead to the next section if you’re not interested.
What are react components actually doing?
When reduced down to the crudest explanation possible, React components are basically functions that receive some data (as props, state, and everything in between), and spits out a DOM representation.
The reason why the DOM representation part is important is that it’s not writing directly to the DOM. If you were working with front-end development back when frameworks wrote directly to DOM elements, you probably remember how expensive those operations are - and how easy it is to end up with a horrible performance.
So, what react actually does: it creates a virtual DOM tree. Every time your state changes (by state I mean your application data - both passed down as props or internal component state), react executes all functions/components again and gets an updated virtual DOM tree. The framework then runs a diff between both representations and only updates the DOM where actual changes exist.
This diffing is why React’s performance is so much better than everything that precedes it - it avoids touching the DOM until necessary.
This whole approach works very well for components that don’t contain a whole lot of expensive logic (read: most of your components), but the frequent update cycles can, very quickly, land you in a huge pot of hot water and reduce your application’s performance down to barely usable.
How bad can it get?
I wrote an example here of a React app with horrible performance, but that can be very easily improved with a couple of small steps.
This application is displaying a list of Fibonacci numbers, and including a form where you can add new numbers.
I know the time complexity of calculating Fibonacci numbers via a recursive algorithm is horrible - and that was the goal here. Yes, I know you can get it down to O(n) by implementing a simple for loop that goes from 0 to n, but again, this example is focusing on React’s performance - and how to tune it even when your component’s logic scales horribly.
Without further ado, here’s the code:
import React from 'react';
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const Fibonacci = ({ n }) => (
<li>
<span>{`fib(${n}) = ${fibonacci(n)}`}</span>
</li>
);
const FibList = ({ list }) => (
<ul>
{list.map(item => <Fibonacci key={item.id} n={item.number} />)}
</ul>
);
class Foo extends React.Component {
state = {
list: [...Array(35)].map((_, i) => ({
id: String(i + 1),
number: i + 1,
})),
newItem: NaN,
};
render() {
return (
<div>
<form>
<input
type="text"
value={this.state.newItem || ''}
onChange={e => this.setState({ newItem: Number(e.target.value) })}
/>
<button
type="submit"
onClick={(e) => {
e.preventDefault();
if (Number.isNaN(this.state.newItem)) return;
this.setState(state => ({
newItem: NaN,
list: [{
id: String(state.list.length + 1),
number: state.newItem,
}, ...state.list],
}));
}}
>
Create New
</button>
</form>
<FibList list={this.state.list} />
</div>
);
}
}
export default Foo;
The performance for the above code (I was actually typing fast! The app ends up with such a bad performance, it literally freezes for a while before coming back with the last update):
Why is it so slow?
It’s all about the update cycle. Remember when I mentioned React runs all the functions again when state changes? What’s happening here is:
- I type into the field
- React fires the event, which triggers a setState in the
Foo
component - Because the
Foo
component changes, React triggers an update for every single child - and the child’s children, and so on - Since everything is being re-rendered, all those expensive Fibonacci calculations are happening at the same time now
Ok, that was bad. How do I fix it?
I’m glad you asked. This is where things start to get interesting.
React gives us two out-of-the-box options to avoid unnecessary updates and one for cases where you need something more involved.
Let’s focus on those out-of-the-box solutions first:
React.memo
React.memo
is a Higher Order Function/Component that basically allows functional components (components that are just a render function as opposed to a class) to only be executed when props change.
It does this by running a shallow equality comparison between old and new props - unless you have nested props, or you re-create objects even if values haven’t changed (this is especially problematic for arrays), React.memo
will solve your problem straight away.
Usage: const MyParagraph = React.memo(({ text }) => <p>{text}</p>);
React.PureComponent
If React.memo
works for functional components, React.PureComponent
solves the issue for class components.
If you’re familiar with the shouldComponentUpdate
lifecycle method, this is going to be an easy explanation: PureComponent inherits from Component and implements shouldComponentUpdate
by running a shallow comparison between old and new props.
Usage: class MyComponent extends React.PureComponent {
.
Putting it together and improving the fibonacci app
// Nothing changed above this line
const Fibonacci = React.memo(({ n }) => ( // <<-- wrap this with React.memo
<li>
<span>{`fib(${n}) = ${fibonacci(n)}`}</span>
</li>
));
const FibList = React.memo(({ list }) => ( // <<-- wrap this with React.memo
<ul>
{list.map(item => <Fibonacci key={item.id} n={item.number} />)}
</ul>
));
class Foo extends React.PureComponent { // <<-- inherit from PureComponent instead of Component
// Nothing changed below this line
How much did it improve it? By a lot, actually:
Can you improve it further?
Of course, you can! There are a few things in this code that are still kinda bad.
Here’s where you can improve it further:
Break the NewNumber out of the Foo component and place it in its own component
The Foo
component is doing too much. Any time a component is having multiple responsibilities, it will have to update everything when one of them changes. If you paid attention to the second gif (the one that shows the good performance), you’ll have noticed that the whole Foo component updates when I type into the input. This is not too bad since the children are now bullet-proof, but it’s still not perfect and leaves potential problems just waiting to happen.
Remove inline function declarations from JSX
There are a few inline functions (which get recreated every time the component gets re-rendered, triggering a false prop change for the children) in the Foo component. We need to make sure this doesn’t happen.
By creating the functions beforehand and just passing them down to whatever components will use them, we ensure that these functions remain the same after each update - which in turns allows React.memo
and React.PureComponent
to do their magic properly.
Sure, just show me the code
Here it goes:
import React from 'react';
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
// Using React.memo to avoid unnecessary updates
const Fibonacci = React.memo(({ n }) => (
<li>
<span>{`fib(${n}) = ${fibonacci(n)}`}</span>
</li>
));
// Using React.memo to avoid unnecessary updates
const FibList = React.memo(({ list }) => (
<ul>
{list.map(item => <Fibonacci key={item.id} n={item.number} />)}
</ul>
));
// Using React.PureComponent to avoid unnecessary updates
class NewNumberForm extends React.PureComponent {
state = {
newItem: NaN,
};
// Declaring this method up here in order to avoid re-creating this function on every update cycle
handleInputChange = e => this.setState({ newItem: Number(e.target.value) });
// Declaring this method up here in order to avoid re-creating this function on every update cycle
handleSubmit = (e) => {
e.preventDefault();
this.props.onSubmit(this.state.newItem);
this.setState({ newItem: NaN });
};
render() {
return (
<form>
<input
type="text"
value={this.state.newItem || ''}
onChange={this.handleInputChange}
/>
<button
type="submit"
onClick={this.handleSubmit}
>
Create New
</button>
</form>
);
}
}
class Foo extends React.PureComponent {
state = {
list: [...Array(35)].map((_, i) => ({
id: String(i + 1),
number: i + 1,
})),
};
handleNewNumberSubmit = (newNumber) => {
if (Number.isNaN(newNumber)) return;
this.setState(state => ({
list: [{
id: String(state.list.length + 1),
number: newNumber,
}, ...state.list],
}));
}
// This component now just renders both NewNumberForm and FibList
// meaning updates to the form are going to be localised in the form itself,
// preventing unnecessary updates from happening in the future when new devs
// add more things to this component
render() {
return (
<div>
<NewNumberForm onSubmit={this.handleNewNumberSubmit} />
<FibList list={this.state.list} />
</div>
);
}
}
export default Foo;
And the end result:
As you can see, updates are now only happening where they actually should. There wasn’t a massive difference between this and the previous step, but you ended up with a better architecture overall that will scale very well.
What about shouldComponentUpdate
I glossed over this, but if you understand PureComponent, you understand shouldComponentUpdate. The idea here is that you can be a lot more targeted. Example:
class MyComponent extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.text !== this.props.text;
}
render() {
return (
<p>{this.props.text}</p>
);
}
}
This is a very dumb example of course, but it’s just to show how specific your logic can be here. You can tell React exactly when to perform an update, and it’ll ignore all other instances.
This approach is usually only necessary for super complex components though - and IMO if you’re faced with something like this, there’s a massive chance you’d be better just refactoring your component into smaller pieces. Nonetheless, it’s a good option for your toolbelt.
The danger with arrays
You can get in trouble very quickly when dealing with arrays. One example is when using redux-forms
, more specifically the validate
options in the Field
component.
Example:
<Field
name="fullName"
label="Full name"
component={renderInput}
validate={[required, minLength(3)]}
/>
Notice how the validate
prop is declaring an array of validators. Every single time this field is updated, validate
will get a new array with the same items - which will trigger React to believe the props have changed again, triggering a new update. This can literally lead to a stack overflow (I’ve seen it happen) in worst case scenarios.
Since that array shouldn’t change, just declare it somewhere else and pass that variable/const down as the prop - headache avoided.
Final thoughts
This is obviously not the ultimate guide on everything you can do to improve your app’s performance, but if you follow these tips you can both improve your existing app, as well as lay a better foundation for scaling your application as the requirements get increasingly complex.
If you have any other tips or questions, please leave them in the comments below!