Optimizing React's Performance
Table of contents
- Maintain Local Component State where Necessary
- Making Use of Immutable Data Structures
- React Components Should be Remembered to Avoid Needless Re-renders
- Files in Many Chunks
- Use “React.Fragments” to Reduce the Number of HTML Element Wrappers
- React’s Windowing or List Virtualization
- React’s Use of Lazy Image Loading
- Conclusion
Application performance optimization is crucial for developers concerned about maintaining a pleasant user experience to keep users engaged. According to Akamai’s research, developers must build apps optimized for speed; every additional second of load time can lead to a 7% drop in conversions. This article will cover the techniques and explain why these optimization processes are essential and when to utilize them.
In this article, we’ll examine several methods for optimizing user-application and streamlining the development process for React developers.
Maintain Local Component State where Necessary
Maintaining a local component state can optimize the performance of a React application by keeping the Component self-contained and reducing the number of props and states that need to be passed down the component tree. This can minimize re-renders and improve the application’s overall performance.
When to use a local component state can vary depending on the specific use case. Still, generally, it is a good idea to use it when a component needs to maintain its internal state that is not directly related to the application’s global state.
For example, a toggle button component may maintain its local state to track whether it is currently in an on
or off
state, rather than relying on props passed down from a parent component.
It’s also important to note that when using local component state, it’s best to use the setState
method provided by React to update the state, as this will trigger a re-render of the Component and ensure that the Component and any child components are updated correctly.
Additionally, it’s essential to remember that when using a local component state, any state updates should be controlled, with proper validation and handling of errors, to ensure that the Component remains in a consistent and predictable state.
Here is an example of maintaining a local component state in a React application using the useState
hook:
import React, { useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
}
const handleDecrement = () => {
setCount(count - 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
}
export default MyComponent;
This example uses the useState()
hook to create a piece of local state called count. The initial value of the count is 0. Next, the Component has two buttons, one to increment the count and one to decrement the count. Finally, the handleIncrement
and handleDecrement
functions update the count by calling setCount
with the new value.
Here is an example of maintaining a local component state in a React application using the useState
hook and Class Component
import React, { Component } from "react";
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
handleDecrement = () => {
this.setState({ count: this.state.count - 1 });
};
render() {
return (
<div>
<p>Count:{this.state.count}</p>
<button onClick={this.handleIncrement}>Increment</button>
<button onClick={this.handleDecrement}>Decrement</button>
</div>
);
}
}
export default MyComponent;
In this example, the Component has a constructor method that initializes the Component’s state with a count property set to 0. The Component has two methods, handleIncrement
and handleDecrement
, that update the count by calling setState
with the new value.
Maintaining a local component state can be a powerful tool for optimizing the performance of a React application. Therefore, it’s a good idea to use it when a component needs to maintain its internal state that is not directly related to the application’s global state. However, it’s essential to use it controlled and ensure that state updates are done correctly.
Making Use of Immutable Data Structures
Using immutable data structures in a React application can improve performance by allowing the application to take advantage of reference equality checks. When a component’s props or state are changed, React checks if the new values are the same as the previous values by comparing their references. React assumes that the Component does not need to re-render
if the references are the same.
The reference will always be different when using mutable data structures, such as Javascript objects or arrays, even if the data is the same. This causes React to re-render the Component even though it doesn’t need to. By using immutable data structures, such as those provided by the immutable library, the reference will not change when the data is updated, allowing React to avoid unnecessary re-renders.
Using immutable data structures can also help debug and reason about the application’s state, as it is guaranteed that a particular state is not modified at a specific point in time, and the data is only added to a new one.
Another benefit of using immutable data structures is that they can make implementing features such as undo/redo or time-travel debugging easier. For example, with immutable data, you can keep a copy of the previous state and revert to it without worrying about the complexity of undoing all the individual mutations made to the data.
Using immutable data can also help improve your application’s performance when it comes to updating, as it does not require iterating and updating the whole object; it can only update the needed specific property.
However, it’s important to note that using immutable data structures does come with a performance cost. Creating new copies of data can be more expensive than mutating existing data structures. So it’s essential to weigh the benefits against the costs and decide whether or not to use immutable data structures based on the specific needs of your application.
Another essential thing to note is that you must change how you work with the data when using immutable data structures. Instead of modifying the data in place, you will have to create new copies of the data with the desired changes. This can add complexity to your code and make it harder to read and understand.
To mitigate this complexity, you can use libraries like immutable.js or immer.js which allows you to write code that looks like it is modifying the data in place but will create new copies of the data under the hood.
Here is an example of using an immutable data structure, specifically an immutable list implemented using the immutable.js
library, in a React application to improve performance:
import { List } from 'immutable';
Class MyComponent extends React.Component {
state = {
items: List([1, 2, 3])
}
handleAddItem = () => {
this.setState(prevState => ({
items: prevState.items.push(4)
}));
}
render() {
return (
<div>
<button onClick={this.handleAddItem}>Add Item</button>
<ul>
{this.state.items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
}
In this example, we’re using the List class from immutable.js to create an immutable list of items stored in the Component’s state. When the “Add Item” button is clicked, the Component’s handleAddItem
function is called.
Instead of modifying the existing state by directly pushing the new item to the list, the function creates a new state by concatenating the new item to the current list of items using the push method provided by the List class; this will prevent the Component from re-rendering the entire list, which can improve performance.
Using immutable data structures in a React application can improve performance by allowing the application to take advantage of reference equality checks and can also help with debugging and reasoning about the application’s state. However, it’s essential to weigh the benefits against the costs and decide whether or not to use immutable data structures based on the specific needs of your application.
React Components Should be Remembered to Avoid Needless Re-renders
There are several ways to optimize the performance of a React application, one of which is to avoid unnecessary re-renders of components. Re-rendering a component can be expensive in terms of performance, especially if the Component has many child components or performs complex calculations.
One way to avoid unnecessary re-renders is to use the shouldComponentUpdate
lifecycle method. This method lets you control when a component should re-render by comparing the current and next props and state. If the Component’s props and state have not changed, you can return false from this method to prevent the Component from re-rendering.
Another way to avoid unnecessary re-renders is to use the React.memo
Higher-order component. This Component wraps your functional Component and only re-renders the Component if the props change.
Lastly, use the useEffect
hook with the proper dependency array to only re-render the Component when necessary.
Using these techniques, you can significantly improve the performance of your React application by avoiding unnecessary re-renders.
Files in Many Chunks
One way to optimize the performance of a React application is to split the files into many chunks, also known as code splitting. This technique allows the browser only to load the code needed for the current page rather than loading all of the code at once. This can improve the initial load time of the application, as well as the overall performance, by reducing the amount of JavaScript that needs to be parsed and executed.
An example of code splitting in a React application would be using the react-loadable, library to dynamically load components as they are needed.
Here is an example of how you might use react-loadable to code split a component:
import Loadable from react-loadable;
const MyComponent = Loadable({
loader: () => import('./MyComponent'),
loading: () => <div>Loading...</div>,
});
export default MyComponent;
In this example, MyComponent
will be loaded when needed, at which point the loader function will be called, and the Component will be imported. The loading component will be displayed while the Component is being loaded.
Use “React.Fragments” to Reduce the Number of HTML Element Wrappers
React.Fragment
is a component that allows you to group a list of children without adding extra nodes to the DOM
. Using React.Fragment
can help reduce the number of HTML
element wrappers in your application and improve performance by reducing the amount of DOM
manipulation React has to do. This can be especially useful when you have a component that needs to render multiple children elements, but you want to leave the wrapper element out of them.
Using React.Fragment
instead of a wrapper element can also make your code more readable and maintainable by making it clear that the element is only being used for grouping purposes and not adding any additional styling or functionality. You can use React.Fragment
in two ways:
Using
<React.Fragment>
<React.Fragment> <Child1 /> <Child2 /> <Child3 /> </React.Fragment>
Using the shorthand
<> </>
<> <Child1 /> <Child2 /> <Child3 /> </>
It is important to note that React.Fragment
does not add any extra nodes to the DOM
and has no additional overhead. It is a simple component that is useful for improving the readability and maintainability of your code.
React’s Windowing or List Virtualization
React’s windowing, or list virtualization, is a technique that improves the performance of large lists by only rendering the items currently visible on the screen rather than rendering the entire list at once. This is done by only rendering the items visible in the viewport and then updating the items as the user scrolls.
This approach can greatly improve the performance of a React application, especially when dealing with large lists of items, as it reduces the amount of work that the browser needs to do to render the list. It also saves memory and helps maintain a smooth scrolling experience for the user.
One popular library for implementing list virtualization in React is react-virtualized. This library provides a set of components that can quickly implement list virtualization in a React application. It also offers a collection of utilities for calculating the size and position of items in a list and handling scroll events.
Another library is react-window, a popular library for implementing windowing in React, providing a set of components for efficiently rendering large lists and tabular data.
When using list virtualization, it’s also essential to consider the overall architecture of the application and how it interacts with the virtualized list. For example, if the data for the list is being loaded from an external source, it’s essential to make sure that the data is being loaded in a way optimized for use with a virtualized list. This may include using pagination or loading data in chunks rather than all at once.
List virtualization is a technique that can significantly improve the performance of a React application when dealing with large lists of items. However, it can be implemented using libraries like react-virtualized and react-window, and it’s essential to consider the overall architecture of the application when implementing it.
React’s Use of Lazy Image Loading
Lazy loading images in a React application can improve performance by only loading images currently visible on the screen. This can reduce the page’s initial load time and the amount of data that needs to be transferred over the network. It improves your React application’s performance by reducing the amount of data that needs to be transferred over the network, which leads to a better user experience.
One way to implement lazy loading in a React application is to use the React lazy and Suspense components. These components allow you to dynamically load components and modules as they are needed rather than loading all components and modules upfront. This can be especially useful for images, as they are often large in file size and take a long time to load.
Another way to implement this technique is by using a library like react-lazyload
, which allows you to easily lazy load images and other elements in your React application. This can be useful if you want to avoid manually implementing lazy loading or if you want to take advantage of additional features like placeholder images and debouncing.
In addition to using React’s built-in lazy loading functionality or a library like react-lazyload
, there are a few other best practices that you can follow to optimize the performance of your React application’s images.
One of these best practices is compressing and optimizing your images before using them in your application. This can reduce the file size of the images and make them load faster. Many tools are available for compressing and optimizing images, such as TinyPNG
or JPEGoptim
. Another best practice is to use a Content Delivery Network
(CDN)
to serve your images. A CDN
can distribute your images across multiple servers, which can improve the load time of your images by reducing the distance that the data needs to travel to reach the user.
It is crucial to ensure that your images are correctly sized and scaled for the devices on which they will be displayed. Loading large images on small screens can slow down the performance of your application and negatively impact the user experience.
Conclusion
Since many factors can cause delays in our application, optimizing a React application necessitates a variety of optimization implementations. However, we have highlighted some critical aspects above, so you do not need a long list if you strictly adhere to the list above. I appreciate your time for reading.
Subscribe to my newsletter
Read articles from HUL Bunsal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
HUL Bunsal
HUL Bunsal
I'm a junior software engineer, I am an enthusiastic and aspiring professional in the field of software development. I collaborate with experienced developers and contribute to the design, coding, testing, and debugging of software applications. With a strong foundation in programming languages and development frameworks, I am eager to learn and grow my skills in areas such as front-end or back-end development. I actively participate in team discussions, follow best coding practices, and continuously seek opportunities to enhance my technical expertise. As a junior software engineer, I play a vital role in the development lifecycle, assisting in the creation of innovative software solutions while gaining valuable hands-on experience in a dynamic and supportive environment.