React Performance Optimization: Complete Guide to Building Fast Applications

React is fast by default, but real-world applications can suffer from inefficient renders, large bundles, and unnecessary computations. Performance optimization requires understanding how React works and identifying bottlenecks.

Key performance metrics include Time to First Contentful Paint (FCP), Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and First Input Delay (FID). Tools like Lighthouse and Chrome DevTools help measure these metrics.

Performance Philosophy:
Measure first, optimize second. Don’t optimize prematurely. Use profiling tools to identify actual bottlenecks before applying fixes.

Optimizing Renders with Memoization

useMemo Hook

Memoize expensive computations to prevent recalculation on every render:

import { useMemo } from ‘react’;function ExpensiveComponent({ data, sortOrder }) {
// Expensive computation
const sortedData = useMemo(() => {
console.log(‘Sorting data…’);
return […data].sort((a, b) => {
return sortOrder === ‘asc’
? a.value – b.value
: b.value – a.value;
});
}, [data, sortOrder]); // Only recalculate when dependencies change

return (
<ul>
{sortedData.map(item => (
<li key={item.id}>{item.name}: {item.value}</li>
))}
</ul>
);
}

useCallback for Function Memoization

import { useCallback } from ‘react’;function Parent({ userId }) {
// Memoize callback to prevent child re-renders
const handleUserUpdate = useCallback((newName) => {
return fetch(`/api/users/${userId}`, {
method: ‘PUT’,
body: JSON.stringify({ name: newName })
});
}, [userId]); // Only recreate if userId changes

return <Child onUpdateUser={handleUserUpdate} />;
}

React.memo for Component Memoization

const ProductCard = React.memo(({ product, onSelect }) => {
console.log(‘Rendering product:’, product.id);return (
<div onClick={() => onSelect(product.id)}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
});

export default ProductCard;

// With custom comparison
const CustomCard = React.memo(
({ product, onSelect }) => <div>…</div>,
(prevProps, nextProps) => {
// Return true if props are equal (skip render)
return prevProps.product.id === nextProps.product.id;
}
);

✅ Memoization Rule: Only memoize when you can measure performance improvement. Memoization has overhead—use it for expensive computations or frequently re-rendering components.

Code Splitting and Lazy Loading

Route-Based Code Splitting

import { lazy, Suspense } from ‘react’;
import { BrowserRouter, Routes, Route } from ‘react-router-dom’;// Load components only when routes are visited
const Dashboard = lazy(() =>
import(‘./pages/Dashboard’)
);
const Analytics = lazy(() =>
import(‘./pages/Analytics’)
);
const Settings = lazy(() =>
import(‘./pages/Settings’)
);

function App() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path=”/” element={<Dashboard />} />
<Route path=”/analytics” element={<Analytics />} />
<Route path=”/settings” element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

Component-Level Code Splitting

import { lazy, Suspense } from ‘react’;const HeavyChart = lazy(() =>
import(‘./components/HeavyChart’)
);

function Dashboard() {
const [showChart, setShowChart] = React.useState(false);

return (
<div>
<button onClick={() => setShowChart(!showChart)}>
Toggle Chart
</button>

{showChart && (
<Suspense fallback={<p>Loading chart…</p>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}

Dynamic Imports

function setupPayment() {
// Load payment library only when needed
import(‘stripe-js’).then((stripe) => {
// Initialize Stripe
});
}// Or with async/await
async function handleCheckout() {
const { stripeModule } = await import(‘./stripe’);
return stripeModule.processPayment();
}

Bundle Analysis and Optimization

Analyzing Bundle Size

// Install bundle analyzer
npm install –save-dev vite-plugin-visualizer// vite.config.ts
import { visualizer } from ‘vite-plugin-visualizer’;

export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
]
};

// Run: npm run build

Tree Shaking

// ❌ Don’t: Default imports (harder to tree-shake)
import lodash from ‘lodash’;
const result = lodash.map([…]);// ✅ Do: Named imports (tree-shakeable)
import { map } from ‘lodash-es’;
const result = map([…]);

// ✅ Even Better: Use modern alternatives
const result = […].map(x => x);

Dependency Optimization

Problem Solution Impact
Moment.js (67KB) Use date-fns or Day.js -60KB
Lodash (73KB) Use lodash-es or native JS -50KB+
Multiple polyfills Target modern browsers -30KB+
Unused CSS frameworks Tailwind or custom CSS -100KB+

Profiling and Measurement

React Profiler API

import { Profiler } from ‘react’;function onRenderCallback(
id, // Component name
phase, // ‘mount’ or ‘update’
actualDuration, // Time spent rendering
baseDuration, // Estimated render time
startTime, // When render started
commitTime // When render committed
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

function App() {
return (
<Profiler id=”App” onRender={onRenderCallback}>
<Header />
<MainContent />
</Profiler>
);
}

Web Vitals Measurement

import {
getCLS,
getFID,
getFCP,
getLCP,
getTTFB
} from ‘web-vitals’;getCLS(console.log); // Cumulative Layout Shift
getFID(console.log); // First Input Delay
getFCP(console.log); // First Contentful Paint
getLCP(console.log); // Largest Contentful Paint
getTTFB(console.log); // Time to First Byte

Chrome DevTools Performance Tab

// Mark performance measurements
performance.mark(‘data-fetch-start’);// Async operation
await fetchData();

performance.mark(‘data-fetch-end’);
performance.measure(
‘data-fetch’,
‘data-fetch-start’,
‘data-fetch-end’
);

// View in Chrome DevTools Performance tab

State Management Optimization

Context Splitting

// ❌ Don’t: Single context with everything
const AppContext = React.createContext();function Provider({ children }) {
const [user, setUser] = React.useState(…);
const [theme, setTheme] = React.useState(…);
const [notifications, setNotifications] = React.useState(…);
return (
<AppContext.Provider value={{ user, theme, notifications }}>
{children}
</AppContext.Provider>
);
}

// ✅ Do: Split into separate contexts
const UserContext = React.createContext();
const ThemeContext = React.createContext();
const NotificationContext = React.createContext();

// Components only re-render when their context changes

useTransition for Non-Blocking Updates

import { useTransition } from ‘react’;function SearchUsers() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = React.useState();
const [results, setResults] = React.useState([]);

const handleChange = (e) => {
const newQuery = e.target.value;
setQuery(newQuery);

// Non-blocking update
startTransition(() => {
const filtered = allUsers.filter(user =>
user.name.includes(newQuery)
);
setResults(filtered);
});
};

return (
<div>
<input onChange={handleChange} />
{isPending && <p>Searching…</p>}
<ResultsList results={results} />
</div>
);
}

Image and Asset Optimization

Responsive Images

function OptimizedImage({ src }) {
return (
<picture>
<source srcSet={`${src}-large.webp`} media=”(min-width: 1024px)” type=”image/webp” />
<source srcSet={`${src}-medium.webp`} media=”(min-width: 640px)” type=”image/webp” />
<source srcSet={`${src}-small.webp`} type=”image/webp” />
<img
src={`${src}.jpg`}
alt=”Optimized”
loading=”lazy”
decoding=”async”
/>
</picture>
);
}

Image Component with Next.js

import Image from ‘next/image’;function ProductImage() {
return (
<Image
src=”/product.jpg”
alt=”Product”
width={500}
height={500}
priority={false} // Lazy load by default
sizes=”(max-width: 768px) 100vw, 50vw”
/>
);
}

Dynamic Imports for Large Libraries

const PDFViewer = React.lazy(() =>
import(‘./PDFViewer’) // Only loaded when needed
);function Document({ showPDF }) {
return (
<div>
{showPDF && (
<Suspense fallback={<p>Loading PDF…</p>}>
<PDFViewer />
</Suspense>
)}
</div>
);
}

List Virtualization

React Window for Large Lists

import { FixedSizeList } from ‘react-window’;function LargeList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);

return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width=”100%”
>
{Row}
</FixedSizeList>
);
}

// Renders only visible items – 10,000+ items with smooth scrolling

Intersection Observer for Lazy Loading

import { useEffect, useRef, useState } from ‘react’;function LazyImage({ src }) {
const ref = useRef();
const [isVisible, setIsVisible] = React.useState(false);

useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(entry.target);
}
});

observer.observe(ref.current);
return () => observer.disconnect();
}, []);

return (
<img
ref={ref}
src={isVisible ? src : }
alt=”Lazy loaded”
/>
);
}

Advanced Optimization Techniques

useReducer for Complex State

const reducer = (state, action) => {
switch (action.type) {
case ‘SET_DATA’:
return { …state, data: action.payload };
default:
return state;
}
};function OptimizedComponent() {
const [state, dispatch] = React.useReducer(
reducer,
{ data: [] }
);

// Stable dispatch function – good for memoization
const setData = useCallback(
(payload) => dispatch({ type: ‘SET_DATA’, payload }),
[]
);

return <Child onSetData={setData} />;
}

Concurrent Rendering with startTransition

import { startTransition } from ‘react’;function FilterList({ items }) {
const [filter, setFilter] = React.useState();

const handleFilterChange = (e) => {
const value = e.target.value;

// Urgent: Update input immediately
setFilter(value);

// Non-urgent: Filter list can be interrupted
startTransition(() => {
// Expensive filtering operation
});
};

return <>…</>;
}

Server-Side Rendering Optimization

// next.config.js
export default {
compress: true,
swcMinify: true,
images: {
formats: [‘image/avif’, ‘image/webp’],
},
headers: async () => [
{
source: ‘/:path*’,
headers: [
{
key: ‘Cache-Control’,
value: ‘public, max-age=31536000’
}
]
}
]
};
✅ Performance Wins: Small optimizations compound. A 100ms improvement in page load time can increase conversion rates by 1%. Focus on real bottlenecks identified through measurement.
© 2025 JavaScript UX. React Performance Optimization Tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *