This article will teach a few ways to optimize the performance of React.js applications. React.js helps us create faster UIs. However, if not managed properly, it can slow down the app (e.g. — due to unnecessary re-renders of a component).
To improve the performance of any application, we first need to measure and identify places in our app which is slower than a defined threshold value. We must then further investigate & mitigate those areas and make fixes.
Below are some resources that I used in my professional life to measure performance, followed by techniques I used to optimize my react applications.
web-vitals
, react-addons-perf
.Quick Tips
1. We must take multiple readings to make sure that the results are authentic and are not under the influence of any other external factor.
2. We can keep an eye on the web console to see the warnings (during development mode). The warnings can sometimes be beneficial and help us improve our app’s overall quality.
3. We must keep an eye on the costly re-renders. There can be few places in our code that would have provoked unnecessary re-renders of a component.
TLDR: A short version of these techniques was originally published by me in WeAreCommunity.
A react component renders when there is a change in props
or state
. Overriding shouldComponentUpdate()
will help us control and avoid any unnecessary re-renders.
shouldComponentUpdate()
is triggered before re-rendering a component.
We will compare the current and next props
& state
. Then, return true
if we want to re-render; else, return false
to avoid a re-render.
function shouldComponentUpdate(next_props, next_state) {
return next_props.id !== this.props.id;
}
Any update triggered in a higher-level component (like A1 in the below image) will also trigger an update for its child components, resulting in degraded performance.
Therefore, adding a check at a higher-level component and overriding the shouldComponentUpdate()
method can be instrumental in a nested component structure and avoid any extra re-renderings.
Instead of overriding shouldComponentUpdate()
method, we can simply create a component that extends from React.PureComponent
.
class ListOfBooks extends React.PureComponent {
render() {
return <div>{this.props.books.join(',')}</div>;
}
}
Disadvantage?
It makes a shallow comparison between current and previous props & states, and creates bugs when handling more complex data structures, like nested objects.
Example:
class ListOfBooks extends React.Component {
constructor(props) {
super(props);
this.state = {
books: ['rich dad poor dad']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This way is not recommended
const books = this.state.books;
books.push('good to great');
this.setState({books: books});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfBooks books={this.state.books} />
</div>
);
}
}
The issue is that PureComponent
will make a simple comparison between the old & new values of this.props.books
.
Since in the handleClick()
method, we are mutating the books
array, the old and new values of this.props.books
will compare as equal, even though actual words in the array have changed.
How to avoid this?
Use immutable data structures along with the use of React.PureComponent
to automatically check for complex state changes.
The above method handleClick()
can be re-written as either of the below –
Using concat
syntax
handleClick() {
this.setState(state => ({
books: state.books.concat(['think and grow rich'])
}));
}
Using spread
syntax
handleClick() {
this.setState(state => ({
books: [...state.books, 'think and grow rich'],
}));
};
Similarly, in the case of object
, we can either use Object.assign()
or spread
syntax to not mutate the objects.
\\this is not recommended - this mutates
function updateBookAuthorMap(bookAuthorMap) {
bookAuthorMap.goodtogreat = 'James';
}\\recommended way - without mutating
function updateBookAuthorMap(bookAuthorMap) {
return Object.assign({}, bookAuthorMap, {goodtogreat: 'James'});
}\\recommended way - without mutating - object spread syntax
function updateBookAuthorMap(bookAuthorMap) {
return {...bookAuthorMap, goodtogreat: 'James'};
}
Quick Tip
While dealing with a deeply nested object, updating them in an immutable way can be very challenging.
For such cases, few libraries let us write a highly readable code without losing its benefits of immutability, like —
immer
,immutability-helper
,immutable.js
,seamless-immutable
,rect-copy-write
.
React.Fragments
help us organize a list of child components without adding additional nodes in the DOM.
In the below image, we can see a clear difference between the number of nodes when we use React.fragments
vs when we do not.
//Sample
export default function App() {
return (
<React.Fragment>
<h1>Hello Component App</h1>
<h2>This is a sample component</h2>
</React.Fragment>
);
}//Alternatively, we can also use <> </> to denote fragments
export default function App() {
return (
<>
<h1>Hello Component App</h1>
<h2>This is a sample component</h2>
</>
);
}
You can fork this code sandbox to test for yourself.
We can use the lodash
library and its helper functions — throttle
and debounce
.
For Example — Refer to myCodeSandbox Example
We can use the memoize technique to store the result of any expensive function call and return the cached result.
This technique will help us optimize the speed of functions whenever the same execution occurs (i.e., if a function is called with the same values as the previous one, then instead of executing the logic, it would return the cached result).
We can use the following ways to memoize in ReactJs –
5.1 Rect.MemoReact.Memo will memoize the component once and will not render it in the next execution as long as the props remain the same.
const BookDetails = ({book_details}) =>{
const {book_title, author_name, book_cover} = book_details;
return (
<div>
<img src={book_cover} />
<h4>{book_title}</h4>
<p>{author_name}</p>
</div>
)
}//memoize the component
export const MemoizedBookDetails = React.memo(BookDetails)
//React will call the MemoizedBookDetails in first render
<MemoizedBookDetails
book_title="rich dad poor dad"
author_name="Robert"
/>//React will not call MemoizedBookDetails on next render
<MemoizedBookDetails
book_title="rich dad poor dad"
author_name="Robert"
/>
5.2 React Hook useMemoIt helps avoid the re-execution of the same expensive function in a component. This hook will be most useful when we pass down a prop in a child component in an array or object, then useMemo will memoize the values between renders.
Example –
import { useState, useMemo } from 'react';
export function CalculateBookPrice() {
const [price, setPrice] = useState(1);
const [increment, setIncrement] = useState(0);
const newPrice = useMemo(() => finalPrice(number), [number]);
const onChange = event => {
setPrice(Number(event.target.value));
};
const onClick = () => setIncrement(i => i + 1);
return (
<div>
New Price of Book
<input type="number" value={price} onChange={onChange} />
is {newPrice}
<button onClick={onClick}>Re-render</button>
</div>
);
}
function finalPrice(n) {
return n <= 0 ? 1 : n * finalPrice(n * 0.25);
}
5.3 moize library to memoize any pure methods
This is a memoization library for JavaScript.
Example –
import moize from 'moize';const BookDetails = ({book_details}) =>{
const {book_title, author_name, book_cover} = book_details;return (
<div>
<img src={book_cover} />
<h4>{book_title}</h4>
<p>{author_name}</p>
</div>
)
}export default moize(BookDetails,{
isReact: true
});
useCallback(function, dependencies)
can help us return a memoized instance of the method that changes with the change of dependencies (i.e., instead of re-creating the instance of the function in every render, the same instance will be used)import { useCallback } from 'react';export function MyBook({ book }) {const onItemClick = useCallback(event => {
console.log('You clicked ', event.currentTarget);
}, [book]);return (
<MyBookList
book={book}
onItemClick={onItemClick}
/>
);
}
Quick Tip — We need to make sure that we use the React Hook
useCallback
for relevant cases only and not overuse it in multiple places.
Refer: Don’t Overuse React UseCallback
setTimeout
)//component
export default Books extends React.Component{constructor(props){
super(books);
}state = {
books: this.props.books
}componentDidMount() {
this.worker = new Worker('booksorter.worker.js');
this.worker.addEventListener('message', event => {
const sortedBooks = event.data;
this.setState({
books: sortedBooks
})
});
}doSortingByReaders = () => {
if(this.state.books && this.state.books.length){
this.worker.postBookDetails(this.state.books);
}
}render(){
const books = this.state.books;
return (
<>
<Button onClick={this.doSortingByReaders}>
Sort By Readers Count
</Button>
<BookList books={books}></BookList>
</>
)
}
}// booksorter.worker.js
export default function sort() {
self.addEventListener('message', e =>{
if (!e) return;
let books = e.data;
//sorting logic
postBookDetails(books);
});
}
In the above code, we execute the sort method in a separate thread. This ensures that we do not block the main thread.
Use Cases for Web Workers — image processing, sorting, filtering, or any extensive CPU tasks.
Official Reference:Using Web Workers
import()
along with the Lazy Loading technique using React.lazy
.//Normal way
import Book from "./components/Book";
import BookDetails from "./components/BookDetails";//React Lazy way
const Book = React.lazy(() => import("./components/Book"));const BookDetails = React.lazy(() => import("./components/BookDetails"));import("./Book").then(book => {
...//logic
});
The lazy components must be rendered inside Suspense
component. The Suspense
will allow us to display the loading text or any indicator as a fallback while React waits to render the component in the front end.
<React.Suspense fallback={<p>Loading page...</p>}>
<Route path="/Book" exact>
<Book/>
</Route>
<Route path="/BookDetails">
<BookDetails/>
</Route>
</React.Suspense>
react-window
, react-virtualized
, etc.React 18 was released this year to improve application performance with a newly updated rendering engine and many more features.
Refer:React 18 New Features
That’s all. Let me know if you liked this article or if you know any more ways to optimize the performance of the React.js application.
Most of the above examples were from my practical experience, and I hope these will be useful for you.
Source: Levelup