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.
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:
// 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
// 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
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;
}
);
Code Splitting and Lazy Loading
Route-Based Code Splitting
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(‘./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
// 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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’
}
]
}
]
};