React has become the go-to framework for developing modern web applications. This framework was developed by Facebook, and it has been used to power up many applications, such as Netflix, Twitter, Airbnb, etc. As an application grows, the number of its features also increases. Hence, its performance may gradually slow down over time, which, in turn, can negatively impact the user experience. Therefore, optimizing our application’s performance becomes crucial. Fortunately, there are several techniques available in React.js that can help us to achieve this goal.
Use Lazy Loading
Lazy loading is a technique for loading content only when needed, which improves performance by reducing initial page load times. In React, there are several ways to implement lazy loading.
Use React.lazy() and Suspense
React lazy is a feature that allows us to dynamically load a component when needed. We can combine this with Suspense to display a fallback component while the component is being loaded. The implementation would look like this:
import * as React from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const Home = React.lazy(() => import("./pages/Home"));
const About = React.lazy(() => import("./pages/About"));
const Loading = () => {
return <div>Loading...</div>;
};
const router = createBrowserRouter([
{
path: "/",
element: (
<React.Suspense fallback={<Loading />}>
<Home />
</React.Suspense>
),
},
{
path: "/about",
element: (
<React.Suspense fallback={<Loading />}>
<About />
</React.Suspense>
),
},
]);
const App = () => {
return <RouterProvider router={router} />;
};
export default App;
Intersection Observer API
Intersection Observer API is an API that can be used to interact with or observe an intersection of a component within the viewport. For example, we can run a function when the viewport touches some portion of the component we want to observe. In React, there are some libraries that help simplifying this API; one of them is react-intersection-observer.
One of the implementations of the use case is infinite scrolling. Infinite scrolling is a technique to make our web load more content as the user scrolls the page; that’s why it is called Infinite scrolling. Below is an example of Intersection Observer API for Infinite scrolling.
const App = () => {
const [posts, setPosts] = useState([]);
const [pageNum, setPageNum] = useState(1);
const [lastElement, setLastElement] = useState(null);
const [totalPages, setTotalPages] = useState(0)
const observer = useRef();
const fetchPosts = async () => {
/// fetch posts data
};
useEffect(() => {
if (pageNum <= totalPages) {
fetchPosts();
}
}, [pageNum]);
useEffect(() => {
observer.current = new IntersectionObserver((entries) => {
const first = entries[0];
if (first.isIntersecting) {
setPageNum((no) => no + 1);
}
});
}, []);
useEffect(() => {
const currentElement = lastElement;
const currentObserver = observer.current;
if (currentElement && currentObserver) {
currentObserver.observe(currentElement);
}
return () => {
if (currentElement) {
currentObserver.unobserve(currentElement);
}
};
}, [lastElement]);
return (
<div >
{
posts.map((post, i) => {
return i === posts.length - 1 ? (
<div key={post.id} ref={setLastElement}>
<PostCard data={post} />
</div>
) : (
<PostCard data={post} key={post.id} />
);
})}
</div>
);
};
export default App;
In this example, we try to assign the last element of the list as ref “last element” for the observer. So, every time the viewport reaches the last element, it will call the API to fetch the post list.
React Loadable
React Loadable is a third-party library that helps us do lazy loading in a more advanced manner. While “React suspense and lazy” enables lazy loading at the routing level, React Loadable operates at the component level. This library is particularly useful in scenarios where we need to load some components that will be displayed later, such as a modal containing heavy components (chart, map). By lazily loading with this library, we gain more flexibility than route-based splitting. We can read more about this library in this GitHub document.
Code Splitting
Imagine we have one big chunk of file that contains all the pages; the size of the bundle will become large, right? Hence, the pages need to be split into several smaller chunks. We can read about the idea here. Using the technique above (React Lazy and Suspense) can help with code splitting. Code splitting happens when we build or minify our code. When we build this codebase using the example above, we can see that it will produce more chunks.

Compared to the one without code splitting:

We can see that it contains more chunks due to the code splitting using React lazy and suspense.
Avoid unnecessary render
React works with something called virtual DOM. To simplify it, React maintains presentational UIs through declarative APIs, which must be rendered into the desired DOM. When the application starts, it renders everything in the component tree into the virtual DOM, together with the component state. Our application will be treated as a tree; if there is an update on the top node, all the leaves will also be updated. We need to make sure that updates in our component only happen in the smaller tree because re-rendering can be heavy sometimes.

In our application, sometimes we include a heavy component, such as a map or chart, which contains a lot of data. The rule of thumb is that components are re-rendered only if needed. The simplest way to detect re-rendering is by using console.log inside the component. Another way to spot it is by using React development tools.
These contain profiler tools to help us monitor what changes are happening in our application.

Once we have insight into the internal working of the application, we can start minimizing the re-render. Here are several things that we can try:
shouldComponentUpdate
The shouldComponentUpdate function can help us decide whether our component needs to update; for example, we want to update if certain props are changing or just a particular state. Below is an example of shouldComponentUpdate.
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.value !== this.props.value) {
return true;
} else {
return false;
}
}
PureComponent
Pure Component is a component that implements shouldComponentUpdate by default. This component will skip re-render if it receives the same props and state. Below is an example of PureComponent.
class Greeting extends PureComponent {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
React.memo
React memo works similarly to shouldComponentUpdate, but this helps in functional components. React memo can also help to skip rendering if there is no change in the props or state. Below is an example of React memo.
const Greeting =()=>{
return <h1>Hello, {this.props.name}!</h1>;
}
const MemoizedGreeting = React.memo(Greeting)
Production Build
The last thing to do is to minify our application. If we use Create-React-App, the tool already contains a bundler to build and minify our application. Otherwise, we must configure our application with tools like Webpack.js or Parcel.js. This step is essential because the minification process not only reduces the size of our apps but also obfuscates the code base. For more information about production build, please click here.
React-Window or React-Virtualized
React-Window and React-Virtualized are libraries that can help us optimize list rendering in our applications. Suppose we have a lot of data that needs rendering on the same page. In that case, it will cause a performance issue for our application. This library can help render only the necessary data currently viewed in the viewport. For more information about React-Window, please read here.
Use Server-side Rendering
We know that React renders the application on the client’s browser. The process happens when the client downloads the necessary file (html, css, and js), and then it receives the React code. Then, React will render the UI on the browser. Server-side rendering means the server will render the UI before sending the file to the client; hence, the client will receive the rendered UI. SSR can help to optimize our application. For example, if we have a blog or news application, SSR can pre-render all the news: so, it can be delivered to the client faster. One of the technologies that can achieve this is Next.js. By using this framework, we can utilize server-side rendering for our application.
Use a CDN
CDN (Content Delivery Network) is a group of servers that are distributed around the world. Being distributed in many locations, CDN gives advantages, such as better speed to retrieve data. CDN is commonly used for hosting things that need to be loaded faster, such as images or assets. It can help us optimize the download speed for our web applications. Many cloud providers offer CDN services: one of them is AWS.
Conclusion
Performance plays a vital role in our application. As the application gets larger, its responsiveness may become the main issue when using the app. However, improvements are achievable to enhance the whole performance of the application. We have already learned some techniques to improve the efficiency of our React application, and with this optimization, we expect that it will increase the user experience for our users. It is part of our duty to take care of it so that it can add more value to our customers.
References:
https://github.com/jamiebuilds/react-loadable
https://reactjs.org/docs/code-splitting.html
https://beta.reactjs.org/learn/react-developer-tools
https://github.com/bvaughn/react-window
Author: Muhammad Aditya Hasry, Software Engineer Programmer