MWAN MOBILE

×
mwan_logo
blog-banner

React.js Performance Optimization Techniques

Application Development 14-Sep-2023

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.

Photo by olia danilevich

Performance Measuring Techniques in React.js

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.

How does React work internally?

Photo by MART PRODUCTION

Performance Optimization Techniques in React.js

TLDR: A short version of these techniques was originally published by me in WeAreCommunity.

1. Overriding shouldComponentUpdate lifecycle method

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.

Nested Component Structure

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.

2. Using React.PureComponent

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-helperimmutable.jsseamless-immutablerect-copy-write.

3. Using React Fragments

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.

Left Side: Use of Fragments | Right Side: Without the use of fragments
//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.

4. Throttling & Debouncing Event actions

  • Identify the event handlers in our code that are expensive or executed many times (for example — scrolling, mouseover, DOM manipulations, processing large lists, etc.)
  • In such scenarios, Throttling and Debouncing will be lifesavers without making significant changes to the event handlers.
  • Throttling — executes any function after a specified time has elapsed and helps restrict the calls to the functions.
  • Debouncing — prevents firing any event too often, i.e., it does not call the function until a defined duration has passed after its previous call.

We can use the lodash library and its helper functions — throttle and debounce.

For Example — Refer to myCodeSandbox Example

5. Memoize React Components

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
});

6. Using the React Hook useCallback

  • In React, when a component re-renders, every method is generated again.
  • 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)
  • Example — A good use case would be when we want to render an extensive list of items.
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

7. Using Web Workers for CPU Extensive Tasks

  • Web Workers run a script in the background, separate from the main execution thread.
  • This background thread will help the main thread (the UI) to run without being blocked or without any lag.
  • Since JavaScript is single-threaded, we need to compute the expensive operation parallelly using either of the below methods –
    A. Psuedo-Parallelism (using setTimeout)
    B. Web Workers
  • Below is an example of using Web Workers
//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

8. Code-splitting using dynamic import()

  • When a react application renders in a browser, then bundled file containing the entire code of the application loads and is served to the user.
  • Bundling is helpful as it reduces the number of server requests a page can handle.
  • The size of the react files increases with the increase in the size of the application. Thus, increasing the size of the bundle. This increase might slow down the initial load of the page.
  • To split a big bundle file into multiple chunks, we can use dynamic 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>

9. Virtualizing / Windowing Long List Data

  • To render an extensive data list, we must not render the entire list in one go. Instead, render only a small part of the list at a time inside the visible viewport.
  • Render more data as the user scrolls, and to achieve this, we can use various libraries like react-windowreact-virtualized, etc.

10. New improvements in React v 18 (released in March 2022)

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

Conclusion

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