Optimize Performance and Render Websites Superfast in ReactJS
In order to optimize performance of your React code, you have to make sure that it’s not re-rendering unnecessarily. This is the main reason for bottlenecks.
Join the DZone community and get the full member experience.
Join For FreeTo optimize the performance of your React code, you have to make sure that it’s not re-rendering unnecessarily. This is the main reason behind performance bottleneck. Use these tips to improve your codebase and make your website super-fast:
- Avoid unnecessary code re-rendering.
- Keep your code files short and break them into components.
- Use minified production build.
- Remove console logs from production code.
- For big tables, render a few rows and columns at a time (like FlatList in React Native).
- Do not mutate state object.
Let’s discuss all these points, one at a time. Buckle up.
Avoid Unnecessary Code Re-rendering
To avoid re-rendering, we use useCallback
or useMemo
in functional components and shouldComponentUpdate
in class-based components. But frankly, we should avoid using these functions unless and until we are getting serious problems like the unresponsiveness of browsers.
Let me tell you how it all works. Suppose I bought a pair of shoes and I don’t want them to be dirty. So, I started walking with shoe covers on them. When I reach home, I throw away the covers, and my shoes are clean. I am happy. But in this whole process, I increased my expenditure on covers and the trouble of putting them all the time when I have to move out.
Now, it's actually a balance between making my shoes dirty and increasing my troubles. If the dirt is light, why not simply clean the shoe with water? Why would I increase my expenditures on covers?
Performance optimization works in the same way. All the methods come with their own overheads. useMemo
is good when the task is computationally heavy; otherwise, let the system compute when it is required.
Why Shouldn’t We Use useMemo
Frequently?
There are some excellent reasons for that:
- It comes with its own overhead.
- It makes your code unnecessarily complex for your teammates.
- Since it caches the results (memoization), the garbage collector won’t run on it and end up using memory for no reason. We are saving computations but sacrificing memory. But is the computation needing to be saved in our code? That decides whether to use it or not.
Some re-rendering is fine but wasteful rendering should be avoided. Look at these examples:
xxxxxxxxxx
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
return (
<div>
<div>
Enter First Name: <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
Enter Last Name: <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
</div>
<div>
First Name: {firstName}
Last Name: {lastName}
</div>
</div>
);
In this code, I am setting up two input fields for first name and last name. Also, the values will be displayed on the next div block. Whenever we change any input field, the whole section will be re-rendered. Although we do not want to render the last name input field and display field, that’s absolutely fine. We should not over-optimize performance. Check out this code demo:
This demo shows how much time it is taking to render the section. In my case, it was taking 32.72 milliseconds.
Let’s over-optimize this code and check the performance:
xxxxxxxxxx
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const FirstName = React.memo(({firstName}) => {
return (
<div style={{marginBottom:'10px'}}>
First Name: {firstName}
</div>
)
})
const LastName = React.memo(({lastName}) => {
return (
<div style={{marginBottom:'10px'}}>
Last Name: {lastName}
</div>
)
})
return (
<div>
<div>
Enter First Name: <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />{ }
Enter Last Name: <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
</div>
<div>
<FirstName firstName={firstName} />
<LastName lastName={lastName} />
</div>
</div>
);
I have separated the display labels into individual components and memoize them using the memo
function. In this case, the labels won’t re-render unless you change them using their respective input fields. Let’s check the time taken in the demo:
In my case, it is taking 37.7 milliseconds. It’s essentially the same time which it was taking before memoization.
useMemo
andmemo
are two different things. Former is a hook and latter is a function. useMemo is used to memoize computations while memo wraps over the component to decide whether to re-render or not.
Where Can useMemo
Optimize Performance?
When computations are heavy, and we are worried more about it than re-rendering. Look at this code:
Warning: It’s a computation heavy code so use it with caution.
xxxxxxxxxx
const [firstName, setFirstName] = React.useState('');
const [lastName, setLastName] = React.useState('');
const [num, setNum] = React.useState(50000000);
const heavyNumber = (num) => {
let heavyNum = 0;
for(let i=0; i<num; i++){
heavyNum += num;
}
return heavyNum;
}
return (
<div>
<div>
Enter First Name: <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> <br />
Enter Last Name: <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} /> <br />
Enter Some Number: <input type="number" value={num} onChange={(e) => setNum(parseInt(e.target.value) < 50000000 ? parseInt(e.target.value): num)} />
</div>
<div>
First Name: {firstName} <br />
Last Name: {lastName} <br />
Computation Heavy Number: {heavyNumber(num)}
</div>
<div style={{marginTop: '30px'}}>
Time Spent in Rendering ~ {timeDiff} milliseconds
</div>
<div style={{marginTop: '30px'}}>
Type fast in input field (first name or last name) and check the time spent
</div>
</div>
);
Here the rendering will lag. This is because the heavyNumber
will be calculated on each render, and that’s unnecessary. Calculate the time it takes to render if you change First Name or Last Name only:
It is taking nearly ~120 milliseconds, and if you type longer then it may get unresponsive. Now let's optimize its performance by using useMemo
x
const [firstName, setFirstName] = React.useState('');
const [lastName, setLastName] = React.useState('');
const [num, setNum] = React.useState(50000000);
const heavyNumber = React.useMemo(() => {
let heavyNum = 0;
for(let i=0; i<num; i++){
heavyNum += num;
}
return heavyNum;
}, [num])
<div>
<div>
Enter First Name: <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> <br />
Enter Last Name: <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} /> <br />
Enter Some Number: <input type="number" value={num} onChange={(e) => setNum(parseInt(e.target.value) < 50000000 ? parseInt(e.target.value): num)} />
</div>
<div>
First Name: {firstName} <br />
Last Name: {lastName} <br />
Computation Heavy Number: {heavyNumber}
</div>
<div style={{marginTop: '30px'}}>
Time Spent in Rendering ~ {timeDiff} milliseconds
</div>
<div style={{marginTop: '30px'}}>
Type fast in input field (first name or last name) and check the time spent
</div>
</div>
In this code, if you change the first name and last name input fields, then you will not find any unresponsiveness or lag in rendering. If you change the number field only then, it will re-compute the value of the heavyNumber
variable. Also, it will store the previously computed values. So, if you enter the number 5000, then it will compute heavyNumber
. If you change it to 5003, it will again calculate heavyNumber
for it. But if you change it back to 5000, then it won’t recalculate. Else it will use the previously calculated number, which is stored already:
Refactor and Break Code into Components
Refactoring a code is important in terms of both code optimization and maintenance. All the reusable components could be separated. In React, we can use refactoring to prevent re-rendering.
Although we have to keep it in balance, sometimes we end up making a lot of small components that are hard to maintain.
Remember, if you got something as an option and not built in, then either it is for maintaining backward compatibility or that “something” is not ideal for general cases.
This code will show you how refactoring can prevent re-rendering:
xxxxxxxxxx
const FirstName = () => {
const [firstName, setFirstName] = useState('');
return (
<div style={{marginBottom: '10px'}}>
Enter First Name: <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} /><br />
First Name: {firstName}
</div>
);
}
const LastName = () => {
const [lastName, setLastName] = useState('');
return (
<div style={{marginBottom: '10px'}}>
Enter Last Name: <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} /><br />
Last Name: {lastName}
</div>
);
}
return (
<div>
<FirstName />
<LastName />
</div>
);
This example is the same as the above ones. Here I have separated the input fields with their respective display labels in different components. The first name data is in <FirstName />
component and last name data in <LastName />
component.
Since the components are different, they do not share state with each other. Changing the first name will not render anything belonging to the last name. Check out the demo:
Use Minified Production Build
Do not use debug code for your production server. Its because all the files are bulky, and a lot of debugging related operations going on in the background.
If you are using the create-react-app
command for creating your application, then run npm build
and it will generate a production-ready build for you. Otherwise, you can directly import the script and css files hosted on unpkg.com cdn:
https://unpkg.com/react@17/umd/react.production.min.js
https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
Remove Console Logs from Production Code
While developing our application, we use a lot of console
properties like logs, error, debug etc. But writing at the console is also overhead for your web application. Its fine during development but doesn’t make sense in production code. In fact, it will reveal way too much information about your application to the end users.
To remove all the console logs from your production build automatically, you can use this Babel Plugin.
Lazy Rendering for Big Tables
You may face rendering issues if your tables are pretty big. We can handle this by lazily rendering the tables like we do in FlatList in React-Native.
There is a library, React-Window, which helps in virtualizing the large lists and lazily render.
Do not Mutate State Object
Mutation of state objects is a bad idea and it could lead to the buggy code. Sometimes this impacts the performance and we look at all the wrong places to optimize it. Always change the state using useState
or setState
functions only.
This is the wrong practice:
x
state = {
name : {first: '', last: ''},
}
this.state.name.first = 'Captain';
this.state.name.last = 'America';
What is wrong here? We are mutating the state directly. This will not update the DOM. React has provided the setState
and useState
functions to understand when its time to render.
Let’s see this in action:
The correct way is using setState
and useState
:
x
state = {
name : {first: '', last: ''},
}
const nameTemp = {this.state.name};
nameTemp.first = 'Captain';
nameTemp.last = 'America';
this.setState({name: nameTemp});
Here we are using the three-dot notation ...
of ES6 to copy the name object in the state. In this codenameTemp
will hold the copy of the name object and not the reference. So, this.state.name
will not be equal to nameTemp
. Any changes you make in a nameTemp
object will not reflect in this.state.name. Then we call this.setState to render with new values.
These are some of the performance optimization strategies you can adopt to make your application fast and maintainable. I hope you learned something. Please share your views in the comment section.
Published at DZone with permission of deven rathore. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments