In order to optimize performance of your React code, you need 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 superfast –
- Avoid unnecessary code re-rendering.
- Keep your code files short and break them in components.
- Use minified production build.
- Remove console logs from production code.
- For big tables, render few rows & columns at a time (Like FlatList in React Native).
- Do not mutate state object.
Let’s discuss all these points.
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, until and unless we are getting serious problems like 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 of covers and troubles of putting them all the time when I have to move out.
Now, its actually a balance between making my shoes dirty and increasing my troubles. If the dirt is less then 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 comes 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 very good reasons for that –
- It comes with its own overhead.
- It makes your code unnecessarily complex for your team mates.
- Since it caches the results (memoization), the garbage collector won’t run on it and we end up using memory for no reason. We are saving computations but sacrificing memory. But is the computation needs 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 this example –
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 last name input field and display field but that’s absolutely fine. We should not over optimize performance. Check out this code demo –
Live 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.
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 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 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 useMemo can 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.
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 lets optimize its performance by using useMemo
–
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 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 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 it stored already.
Demo –
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 which 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 –
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 above ones. Here I have separated the input fields with their respective display labels in different components. The firstname data is in <FirstName />
component and lastname data in <LastName />
component.
Since the components are different, they do not share state with each other. Changing first name will not render anything belonging to 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 debug related operations going on in the background.
If you are using create-react-app
command for creating your application, then run npm build
and it will generate 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 an overhead for your web application. Its fine during development but doesn’t make sense in production code. In fact, it will reveal a 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 object 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 a wrong practice –
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. Clicking on button will not do anything. Check out demo –
The correct way is, using setState and useState. See this –
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 code nameTemp
will hold the copy of name object and not the reference. So, this.state.name
will not be equal to nameTemp
. Any changes you make in nameTemp
object, will not reflect in this.state.name
. Then we call this.setState
to render with new values.
Conclusion
Performance optimization is subject to use case. That’s why we call it optimization. A little re-rendering is absolutely fine. But if it is getting lags then we should use the principles discussed in the article.