React Performance Optimization — Profiling, Memoization, and Bundling
In this tutorial, you'll learn about React Performance Optimization. We cover key concepts, practical examples, and best practices.
What You'll Learn
Optimize React application performance using profiling, memoization (React.memo, useMemo, useCallback), code splitting with React.lazy, windowing for large lists, and bundle analysis with practical metrics.
Why It Matters
Slow React apps lose users. Every additional 100ms of load time reduces conversion by 7%. Unnecessary re-renders, large bundles, and unoptimized lists cause janky interactions and poor Core Web Vitals scores that hurt both user experience and SEO.
Real-World Use
A data dashboard rendering 10,000 rows of financial data uses windowing to render only visible rows, memoization to prevent chart re-computation on unrelated state changes, and code splitting to load the analytics module only when the user navigates to it.
Learning Path
flowchart LR
A[React Basics] --> B[Hooks Deep Dive]
B --> C[Performance Optimization]
C --> D[Code Splitting]
D --> E[Design Patterns]
E --> F[Final Project]
C -->|You are here| C
Profiling with React DevTools
Before optimizing, measure. React DevTools Profiler records render times and identifies which components re-render and why.
import { Profiler, ProfilerOnRenderCallback } from "react";
function onRenderCallback: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
) => {
console.table({
id,
phase, // "mount" or "update"
actualDuration, // time spent rendering this component
baseDuration, // estimated time without memoization
commitTime, // when the commit happened
});
};
function Dashboard() {
return (
<Profiler id="Dashboard" onRender={onRenderCallback}>
<ExpensiveWidget />
</Profiler>
);
}
Expected output: After each render, the browser console shows a table with the Profiler data. Use this to identify components taking more than 5ms to render — those are candidates for memoization.
React.memo — Preventing Unnecessary Re-renders
React.memo wraps a component to skip re-rendering if its props have not changed (shallow comparison).
import { memo, useState } from "react";
interface StockRowProps {
symbol: string;
price: number;
change: number;
}
const StockRow = memo(function StockRow({ symbol, price, change }: StockRowProps) {
console.log(`Rendering ${symbol}`);
return (
<tr>
<td>{symbol}</td>
<td>${price.toFixed(2)}</td>
<td style={{ color: change >= 0 ? "green" : "red" }}>
{change >= 0 ? "+" : ""}{change.toFixed(2)}%
</td>
</tr>
);
});
function StockTable() {
const [filter, setFilter] = useState("");
const stocks = [
{ symbol: "AAPL", price: 178.50, change: 1.2 },
{ symbol: "GOOGL", price: 141.80, change: -0.5 },
{ symbol: "MSFT", price: 378.90, change: 2.1 },
];
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="Filter..." />
<table>
<tbody>
{stocks.map((s) => (
<StockRow key={s.symbol} {...s} />
))}
</tbody>
</table>
</div>
);
}
Expected output: console.log runs once per stock on initial mount. Typing in the filter input does NOT trigger re-renders of StockRow because the stocks array reference has not changed. Without memo, every keystroke would re-render all three rows.
useMemo and useCallback — Expensive Computations and Stable Callbacks
import { useMemo, useCallback, useState, memo } from "react";
interface DataPoint {
timestamp: number;
value: number;
}
function MovingAverage({ data, windowSize }: { data: DataPoint[]; windowSize: number }) {
const [color, setColor] = useState("#007bff");
// Only recompute when data or windowSize changes
const averages = useMemo(() => {
console.log("Computing moving averages...");
const result: number[] = [];
for (let i = 0; i < data.length; i++) {
const start = Math.max(0, i - windowSize + 1);
const slice = data.slice(start, i + 1);
const avg = slice.reduce((s, p) => s + p.value, 0) / slice.length;
result.push(avg);
}
return result;
}, [data, windowSize]);
// Stable callback — same reference across renders
const handleColorChange = useCallback((newColor: string) => {
setColor(newColor);
}, []);
return (
<div>
<ColorPicker onChange={handleColorChange} />
<Chart data={averages} color={color} />
</div>
);
}
Expected output: "Computing moving averages..." appears only when data or windowSize actually change. Changing the color does NOT trigger re-computation. The handleColorChange reference stays stable, preventing ColorPicker from re-rendering unless its other props change.
Code Splitting with React.lazy
Lazy load routes and heavy components so the initial bundle stays small.
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Analytics = lazy(() => import("./pages/Analytics"));
const Settings = lazy(() => import("./pages/Settings"));
function LoadingFallback() {
return <div aria-label="Loading page">Loading...</div>;
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Expected output: The initial bundle contains only the shell and Dashboard code. When the user navigates to /analytics, the Analytics chunk loads on demand. The LoadingFallback renders while the chunk downloads. Network tab shows separate JS chunks for each route.
Windowing with react-window
Rendering thousands of list items at once causes layout thrashing and jank. Windowing renders only the visible items.
import { FixedSizeList as List } from "react-window";
interface LogEntry {
id: number;
level: "info" | "warn" | "error";
message: string;
timestamp: string;
}
const logs: LogEntry[] = Array.from({ length: 100000 }, (_, i) => ({
id: i,
level: i % 10 === 0 ? "error" : i % 5 === 0 ? "warn" : "info",
message: `Log entry number ${i + 1}`,
timestamp: new Date().toISOString(),
}));
function LogRow({ index, style }: { index: number; style: React.CSSProperties }) {
const log = logs[index];
return (
<div style={style}>
<span
style={{
color: log.level === "error" ? "red" : log.level === "warn" ? "orange" : "green",
marginRight: "8px",
}}
>
[{log.level.toUpperCase()}]
</span>
{log.timestamp} — {log.message}
</div>
);
}
function LogViewer() {
return (
<List
height={600}
itemCount={logs.length}
itemSize={35}
width="100%"
>
{LogRow}
</List>
);
}
Expected output: Only ~18 rows are rendered in the DOM at any time, even though the data has 100,000 entries. Scrolling recycles the same DOM nodes with updated content. The page stays at 60fps during rapid scrolling.
Bundle Analysis
Run these commands to analyze your bundle composition:
# Using Vite
npx vite-bundle-visualizer
# Using Create React App
npx source-map-explorer build/static/js/*.js
Expected output: A treemap visualization showing which modules consume the most bytes. Common targets: moment.js (large locale data), lodash (tree-shake!). Replace with date-fns or native Intl where possible.
Common Performance Mistakes
1. Premature Memoization
Wrapping every function in useCallback and every value in useMemo adds overhead. Profile first, then optimize the top 3 bottlenecks.
2. Inline Functions in Memoized Components
Passing an inline arrow function as a prop to a memoized child breaks memoization because a new function reference is created every render. Use useCallback.
3. Not Using Production Builds
React development builds include warnings, propType checks, and slow development mode. Always test performance against a production build.
4. Large Bundles from Unused Imports
Importing only what you need from libraries and enabling tree-shaking in your bundler reduces bundle size.
Practice Questions
Challenge
Build a product listing page that renders 5,000 products with images, titles, prices, and ratings. Profile it with React DevTools. Apply three optimizations: React.memo for product cards, wind owing for the list, and lazy loading for images. Measure the render time before and after.
Real-World Task
Analyze a production React app with the Chrome Performance tab and React DevTools Profiler. Identify the top three components causing re-render bottlenecks. Apply memoization, extract expensive computations with useMemo, and split the largest route with React.lazy. Document the performance improvement with before/after metrics.
This tutorial is built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — performance-critical applications serving millions of users with sub-second interactions.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro