Optimizing Performance in a React Application: A Case Study
During the development of a complex React application, we faced significant performance challenges, particularly with the initial render time. In this article, I will discuss some of the issues we encountered and the strategies we implemented to address them. The primary focus will be on code splitting, infinite scrolling, and error boundaries.
The Challenge
As we progressed with development, it became clear that the initial rendering time of the application was steadily increasing. This had a negative impact on user experience and overall performance, especially as we added more modules and features.
The key issue we needed to tackle was the excessively long initial render time, which stemmed from several factors:
Large JavaScript Bundles: Initially, the application loaded all components and modules in a single bundle, which took considerable time for the browser to process.
Heavy Component Rendering: The integration of multiple resource-intensive modules led to significant data processing and component rendering during the initial load.
Optimization Strategies
To enhance the application's performance and decrease the initial load time, we employed the following optimization techniques:
Code Splitting with React.lazy and Suspense
Code Splitting allows us to break the large application into smaller chunks that can be loaded as needed, thereby reducing the initial load time.
- Dynamic Imports: By utilizing the React.lazy function, we can dynamically import components only when they are required. This approach prevents the need to load all application code at once./
import React, { Suspense } from 'react';
// Components lazy loaded
const MedicalEncounterOPD = React.lazy(() => import('./MedicalEncounterOPD'));
const ReportCharts = React.lazy(() => import('./ReportCharts'));
// Component using lazy-loaded components
const App = () => (
<Suspense fallback={<div>Loading.</div>}>
<MedicalEncounterOPD />
<ReportCharts />
</Suspense>
);
- Code Splitting Benefits: By splitting the code, only the components currently in use are loaded. For example, the
MedicalEncounterOPD
component is only loaded when the user navigates to the relevant section, rather than being included in the initial bundle.
Infinite Scrolling
Infinite Scrolling is a technique that helps handle large datasets by loading data incrementally as users interact with the application. This approach enhances performance by eliminating the need to load all data at once during the initial render.
- Implementation: An infinite scrolling feature was implemented to progressively fetch and display data, which helps reduce load times for data-heavy modules.
import React, { useState, useEffect } from 'react';
const DataList = () => {
const [data, setData] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const loadMoreData = async () => {
// Fetch data based on the current page
const response = await fetch(/api/data?page=${page});
const newData = await response.json();
if (newData.length === 0) {
setHasMore(false);
} else {
setData(prevData => [...prevData, ...newData]);
setPage(prevPage => prevPage + 1);
}
};
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
if (hasMore) {
loadMoreData();
}
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [hasMore]);
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
{hasMore && <div>Loading more...</div>}
</div>
);
};
export default DataList;
Error Boundaries
Error Boundaries are a feature in React that helps developers manage errors in components, ensuring that a single component error doesn't crash the entire application. This contributes to a more reliable user experience.
- Implementation: Error boundaries were implemented around key components to catch rendering errors and show a fallback UI if an error occurs.
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
// Log error details for debugging
console.error('Error caught by ErrorBoundary:', error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong. Please try again later.</h1>;
}
return this.props.children;
}
}
// Usage
const App = () => (
<ErrorBoundary>
<MedicalEncounterOPD />
<ReportCharts />
</ErrorBoundary>
);
Outcome
By implementing these optimization strategies, the application's performance saw a notable boost. Code splitting helped decrease the initial bundle size and loading time, infinite scrolling allowed for better management of large datasets, and error boundaries improved the application's stability.
Utilizing these modern performance optimization techniques significantly enhanced the application's responsiveness and overall user experience, effectively tackling the issues related to initial render delays.
Subscribe to my newsletter
Read articles from Eaysin Arafat directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Eaysin Arafat
Eaysin Arafat
Exploring the syntax of life through code and words.