Boost React App Speed

Boost Your React App Speed with Proven Performance Optimisation Techniques

Your React app was fast when you first built it. Now it lags, load times have crept up, and users are starting to notice. The good news: most React performance problems come from a handful of fixable root causes.

This guide walks you through seven proven techniques to speed up your React app with concrete data, practical code patterns, and clear guidance on when to apply each one. Whether your app is sluggish on initial load, slow to respond to user input, or struggling under large datasets, you will find a targeted fix here.

Before you touch a single line of code, open the React DevTools Profiler and record an interaction that feels slow. The flamegraph view shows you exactly which components are rendering, how long each render takes, and what triggered it. Optimising without this data is guesswork. With it, you can focus your effort where it actually moves the needle.

1. Code Splitting for Faster Loading

By default, your build tool bundles your entire React application into one JavaScript file. Every user downloads all of it even the parts they never visit.

Code splitting breaks that bundle into smaller chunks that load only when needed. React.lazy() combined with Suspense makes route-level splitting straightforward:

import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}

The Dashboard component now loads only when it is rendered, not on the initial page visit. For most apps, route-level code splitting reduces the initial bundle size by 30–50%, directly improving Largest Contentful Paint (LCP). According to the HTTP Archive (2024 data), the median JavaScript payload for desktop users already exceeds 500 KB. Splitting aggressively keeps that number from growing further.

Beyond routes, consider splitting heavy third-party libraries chart libraries, rich text editors, PDF viewers that only appear on specific pages. Use Webpack Bundle Analyzer to visualize your bundle and identify the biggest contributors before deciding where to split.

2. Memoization to Prevent Unnecessary Renders

React re-renders a component every time its parent re-renders, even when that component’s props have not changed. In a dashboard with 50 components, a top-level state change can trigger all 50 to re-render unnecessarily. According to research from Bit.dev, poor render performance can increase scripting time by 30–60%, a measurable hit to both user experience and SEO.

React.memo is your first line of defence. It wraps a component and performs a shallow prop comparison before committing to a re-render:

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
});

React.memo only works when the props it receives have stable references. Pair it with useMemo for computed values and useCallback for event handlers:

function ProductPage({ products }) {
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => b.price - a.price);
}, [products]);

const handleDelete = useCallback((id) => {
// delete logic
}, []);

return <ExpensiveList items={sortedProducts} onDelete={handleDelete} />;
}

When not to memoize: Every React.memo wrapper adds a shallow comparison check on every render. For small, fast-rendering components, that overhead can cost more than simply re-rendering. Profile first with React DevTools, then apply memoization selectively to components with render times above 5–10ms that trigger frequently. Wrapping everything in React.memo Without measurement is a common anti-pattern.

When applied correctly, memoization can reduce re-renders by 30–50% across large lists. One internal audit documented a 40% reduction in re-render count on a product listing page after targeted memoization.

3. Virtualize Large Lists

Rendering a list of 500 items means the browser creates 500 DOM nodes, manages 500 event listeners, and performs layout and paint operations for all of them, even those far outside the visible viewport. The result is slow initial renders, sluggish scrolling, and high memory usage.

Virtualization fixes this by rendering only the items currently visible in the viewport, plus a small buffer. react-window is the most widely used library for this:

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width={300}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}

For a list of 10,000 items, this renders approximately 20 DOM nodes at any given time instead of 10,000. That single change has been documented to reduce list render time from over 3 seconds to near-instant.

Apply virtualisation when your list exceeds 100 items. Below that count, the DOM handles the load without noticeable degradation. Above it, the gains compound as the list grows. Also, make sure you are using stable, unique key props — using the array index as a key causes subtle bugs during reordering and filtering and forces React to re-render the entire list unnecessarily.

4. Optimise Images and Other Assets

Images are typically the largest files on a React page. Large, uncompressed images block page load, inflate LCP times, and waste bandwidth, particularly on mobile networks.

Switch to modern formats. WebP images are 25–35% smaller than JPEGs at equivalent quality. AVIF offers even greater compression where browser support allows. Updating your image pipeline to serve WebP by default is one of the fastest wins available:

<img src="photo.webp" alt="Product image" />

Lazy load offscreen images. The native loading="lazy" attribute tells the browser to defer images until they scroll into view. This reduces initial page weight and prevents offscreen images from competing with critical resources:

<img src="photo.webp" loading="lazy" alt="Product image" />

Serve responsive images. Sending a 2000px image to a 400px mobile screen wastes bandwidth and slows load. Use srcSet and sizes to serve appropriately sized images per viewport:

<img
srcSet="photo-400.webp 400w, photo-800.webp 800w"
sizes="(max-width: 600px) 400px, 800px"
src="photo-800.webp"
alt="Product image"
/>

If you are building with Next.js, the next/image component handles format conversion, responsive sizing, and lazy loading automatically.

5. Use a Production Build and Reduce Bundle Size

Development builds of React include extra warnings, debugging tools, and unminified code none of which belong in production. Always deploy using a production build. In Create React App, that means running npm run build. In Next.js, it means next build. The difference in bundle size is substantial.

Beyond build mode, keep bundle size under control with tree shaking. Modern bundlers (Webpack, Vite, Rollup) eliminate unused code automatically but only if you import precisely what you need:

// Imports the entire lodash library (~70KB)
import _ from 'lodash';

// Imports only the function you need (~2KB)
import debounce from 'lodash/debounce';

That single import change reduces one dependency’s contribution from 70KB to 2KB. Multiply that pattern across 10–15 dependencies and the cumulative reduction is significant.

Use Webpack Bundle Analyzer to find your largest contributors. You will often discover that one or two packages account for a disproportionate share of your bundle and that lighter alternatives exist.

6. Implement Lazy Loading for Components

Lazy loading is closely related to code splitting, but it extends beyond routes. Any component that is expensive to render and not required on initial load is a candidate for lazy loading.

You have already seen React.lazy() In the code splitting section. Apply the same pattern to:

  • Modal dialogs that only appear after a user action
  • Admin panels or settings screens visited infrequently
  • Charts and data visualizations that load only on specific pages
  • Third-party integrations — video players, PDF viewers, maps

Avoid lazy loading components that appear immediately above the fold. If the user sees it on first render, it needs to load immediately; otherwise, you introduce a visible delay that feels like a bug.

For search inputs and other interactions that trigger expensive computations, consider debouncing to limit how often those computations run:

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);

A 300ms debounce window catches most typing pauses without adding noticeable delay. For React applications using React 18 or later, useTransition Offers a React Native alternative that keeps the UI responsive while deferring non-urgent state updates, particularly effective for search-as-you-type patterns.

7. Monitor and Analyse Performance Continuously

Performance is not a one-time task. Features ship, dependencies accumulate, and what is fast today can become a bottleneck in three months. Continuous monitoring is what separates teams that maintain performance from those that rediscover problems after users complain.

Here are the tools to add to your regular workflow:

React DevTools Profiler identifies which components rendered, how long each render took, and what triggered it. Use it during development and after major feature releases to catch regressions before they reach production.

Lighthouse (available in Chrome DevTools) audits your app against Core Web Vitals thresholds: LCP under 2.5 seconds, INP under 200 milliseconds, and CLS below 0.1. Run it in your CI pipeline to prevent performance regressions from reaching production undetected.

Web Vitals JS collects real-user data in production. Lab measurements do not always reflect actual user experiences across different devices and network conditions. Field data does.

Chrome DevTools Performance Tab gives a frame-by-frame view of JavaScript execution time, long tasks, paint and layout events, and frame drops. Use it when Profiler shows a slow component, but you need to understand what is happening at the browser level.

Build profiling into your code review process, especially for shared components and UI kits. Review flamegraphs after major refactors. Set up alerts for Core Web Vitals regressions. The teams that catch performance issues earliest spend the least time fixing them.

A Faster React App Starts with the Right Approach

Speed is not just a technical metric. Every 100 milliseconds of latency has a measurable impact on conversion rates, and sites that pass all three Core Web Vitals thresholds consistently outperform competitors in organic search.

The techniques in this guide, code splitting, memoisation, virtualisation, image optimisation, production builds, lazy loading, and continuous monitoring, address the root causes behind most React performance problems. Apply them systematically, measure before and after each change, and focus your effort on the bottlenecks that Profiler actually surfaces.

Ready to take your React performance further?

Monarch Innovation works with development teams to diagnose performance bottlenecks, implement optimisation strategies, and build faster, more scalable React applications. Explore what Monarch Innovation can do for your project.

FAQs

What is code splitting and how does it speed up a React app?

Code splitting is a technique that divides your React application into smaller JavaScript bundles instead of loading the entire application at once. This allows users to download only the code needed for the current page, reducing initial load time and improving performance. React supports code splitting using React.lazy() and dynamic import().

When should I use React.memo, useMemo, and useCallback?

These React optimization tools help prevent unnecessary re-renders:

  • React.memo: Wrap functional components that receive the same props frequently to avoid unnecessary re-rendering.
  • useMemo: Memoize expensive calculations so they only run when dependencies change.
  • useCallback: Memoize callback functions to prevent unnecessary function recreation, especially when passing functions to child components.

Use them only when profiling indicates performance bottlenecks, as excessive memoization can add unnecessary complexity.

How do I virtualize large lists in React?

Virtualization renders only the visible items in a long list instead of every item in the DOM. This significantly improves rendering performance and reduces memory usage. Libraries such as react-window and react-virtualized are commonly used to efficiently display thousands of rows while maintaining smooth scrolling.

How can I optimize images in a React app?

Image optimization improves page speed and user experience by:

  • Compressing images before deployment.
  • Using modern formats like WebP or AVIF.
  • Serving responsive images with appropriate sizes.
  • Implementing lazy loading for below-the-fold images.
  • Using a CDN for faster image delivery.

These practices reduce bandwidth usage and improve Core Web Vitals.

What is lazy loading and which components should I lazy load in React?

Lazy loading delays loading components until they are actually needed. It reduces the initial bundle size and speeds up the first page load.

Good candidates for lazy loading include:

  • Route-based pages
  • Admin dashboards
  • Settings pages
  • Charts and analytics modules
  • Large forms
  • Modals and dialogs
  • Third-party components that aren’t immediately visible

React provides React.lazy() and Suspense to implement lazy loading efficiently.

How do I continuously monitor React app performance?

Performance monitoring should be an ongoing process. Regularly track metrics such as page load time, interaction responsiveness, bundle size, and Core Web Vitals. Use tools like Lighthouse, React DevTools Profiler, Chrome DevTools Performance panel, and Real User Monitoring (RUM) solutions to identify bottlenecks. Continuous monitoring helps detect regressions early and ensures your React application remains fast as it evolves.

Previous Next
Close
Test Caption
Test Description goes like this
Add to cart