Performance Optimization¶
Comprehensive guide to optimizing React and Next.js applications for production.
Core Optimization Principles¶
The Three Pillars¶
- Minimize Re-renders - Only update when necessary
- Reduce Bundle Size - Ship less JavaScript
- Optimize Data Loading - Fetch efficiently
React Performance Optimization¶
useMemo - Cache Expensive Calculations¶
Purpose: Remember computed values between renders.
When to use: - Expensive calculations - Complex transformations - Object/array creation passed as props
function ProductList({ products, searchTerm }) {
// ❌ Recalculated every render
const filtered = products.filter(p =>
p.name.includes(searchTerm)
);
// ✅ Only recalculates when dependencies change
const filtered = useMemo(() =>
products.filter(p => p.name.includes(searchTerm)),
[products, searchTerm]
);
return <List items={filtered} />;
}
Real-world example:
function Dashboard({ data }) {
const statistics = useMemo(() => {
console.log('Calculating statistics...'); // Only logs when data changes
return {
total: data.reduce((sum, item) => sum + item.value, 0),
average: data.length ? data.reduce((sum, item) => sum + item.value, 0) / data.length : 0,
max: Math.max(...data.map(item => item.value))
};
}, [data]);
return (
<div>
<h2>Total: {statistics.total}</h2>
<h2>Average: {statistics.average}</h2>
<h2>Max: {statistics.max}</h2>
</div>
);
}
useCallback - Memoize Functions¶
Purpose: Keep the same function reference between renders.
When to use: - Functions passed to child components - Functions used in dependency arrays - Event handlers in optimized components
function Parent() {
const [count, setCount] = useState(0);
// ❌ New function every render
const handleClick = () => {
console.log('Clicked');
};
// ✅ Same function reference
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return <ExpensiveChild onClick={handleClick} />;
}
With dependencies:
function SearchBox({ onSearch, apiKey }) {
const handleSearch = useCallback((term) => {
// Uses latest apiKey
onSearch(term, apiKey);
}, [onSearch, apiKey]);
return <SearchInput onSubmit={handleSearch} />;
}
React.memo - Component Memoization¶
Purpose: Skip re-rendering when props haven't changed.
// ❌ Re-renders every time parent renders
function ListItem({ item }) {
return <div>{item.name}</div>;
}
// ✅ Only re-renders when item changes
const ListItem = React.memo(function ListItem({ item }) {
return <div>{item.name}</div>;
});
Custom comparison:
const UserCard = React.memo(
function UserCard({ user }) {
return <div>{user.name} - {user.email}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip render)
return prevProps.user.id === nextProps.user.id;
}
);
Next.js Performance Features¶
Server Components (RSC)¶
Benefits: - Zero JavaScript to client - Direct database access - Automatic code splitting
// app/posts/page.tsx - Server Component by default
async function PostsPage() {
const posts = await db.post.findMany(); // Direct DB access
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Dynamic Imports - Code Splitting¶
Basic usage:
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false // Client-only
});
function Page() {
return <HeavyComponent />;
}
Conditional loading:
function Editor() {
const [showRichText, setShowRichText] = useState(false);
const RichTextEditor = useMemo(() =>
dynamic(() => import('./RichTextEditor')),
[]
);
return (
<div>
<button onClick={() => setShowRichText(true)}>
Enable Rich Text
</button>
{showRichText && <RichTextEditor />}
</div>
);
}
Image Optimization¶
import Image from 'next/image';
// ✅ Automatic optimization
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
priority // Above-the-fold images
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
Responsive images:
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
Font Optimization¶
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter'
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}
Caching Strategies¶
ISR (Incremental Static Regeneration)¶
// Revalidate every 60 seconds
export const revalidate = 60;
async function ProductPage({ params }) {
const product = await fetchProduct(params.id);
return <ProductDetails product={product} />;
}
React.cache (Server Components)¶
import { cache } from 'react';
const getUser = cache(async (id: number) => {
const user = await db.user.findUnique({ where: { id } });
return user;
});
// Called multiple times, fetches once
async function UserProfile({ id }) {
const user = await getUser(id);
return <div>{user.name}</div>;
}
async function UserBadge({ id }) {
const user = await getUser(id); // Cached!
return <span>{user.name}</span>;
}
Fetch Caching¶
// Cache forever
fetch('https://api.example.com/data', {
cache: 'force-cache'
});
// Never cache
fetch('https://api.example.com/data', {
cache: 'no-store'
});
// Revalidate after 60 seconds
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
Streaming and Suspense¶
Progressive Rendering¶
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<Header /> {/* Renders immediately */}
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<AnotherSlowComponent /> {/* Parallel loading */}
</Suspense>
</div>
);
}
Loading States¶
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
// Automatically wrapped in Suspense by Next.js
Data Fetching Optimization¶
React Query Best Practices¶
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
});
if (isLoading) return <Skeleton />;
return <Profile user={data} />;
}
Parallel Queries¶
function Dashboard() {
const users = useQuery(['users'], fetchUsers);
const posts = useQuery(['posts'], fetchPosts);
const stats = useQuery(['stats'], fetchStats);
if (users.isLoading || posts.isLoading || stats.isLoading) {
return <Loading />;
}
return (
<div>
<UsersList data={users.data} />
<PostsList data={posts.data} />
<Statistics data={stats.data} />
</div>
);
}
Prefetching¶
import { useQueryClient } from '@tanstack/react-query';
function ProductList({ products }) {
const queryClient = useQueryClient();
return products.map(product => (
<ProductCard
key={product.id}
product={product}
onMouseEnter={() => {
// Prefetch details
queryClient.prefetchQuery(
['product', product.id],
() => fetchProductDetails(product.id)
);
}}
/>
));
}
Common Performance Anti-Patterns¶
❌ Problem: Rendering Large Lists¶
// Bad - renders 10000 items
function List({ items }) {
return (
<div>
{items.map(item => <Item key={item.id} data={item} />)}
</div>
);
}
✅ Solution: Virtual Scrolling
import { FixedSizeList } from 'react-window';
function List({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<Item data={items[index]} />
</div>
)}
</FixedSizeList>
);
}
❌ Problem: Inline Objects/Arrays¶
// Bad - new object every render
function Parent() {
return <Child style={{ margin: 10 }} options={['a', 'b']} />;
}
✅ Solution: Move outside or memoize
const STYLE = { margin: 10 };
const OPTIONS = ['a', 'b'];
function Parent() {
return <Child style={STYLE} options={OPTIONS} />;
}
// Or with dynamic values
function Parent({ value }) {
const style = useMemo(() => ({ margin: value }), [value]);
return <Child style={style} />;
}
❌ Problem: State in Map Loop¶
// Bad - each item has separate state
function List({ items }) {
return items.map(item => {
const [selected, setSelected] = useState(false); // ❌
return <Item key={item.id} selected={selected} />;
});
}
✅ Solution: Lift state up
function List({ items }) {
const [selectedIds, setSelectedIds] = useState(new Set());
return items.map(item => (
<Item
key={item.id}
selected={selectedIds.has(item.id)}
onSelect={() => {
const next = new Set(selectedIds);
next.add(item.id);
setSelectedIds(next);
}}
/>
));
}
Bundle Size Optimization¶
Analyze Bundle¶
# Next.js built-in analyzer
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// config
});
# Run analysis
ANALYZE=true npm run build
Tree Shaking¶
// ❌ Imports entire library
import _ from 'lodash';
const result = _.chunk(array, 3);
// ✅ Import only what you need
import chunk from 'lodash/chunk';
const result = chunk(array, 3);
// ✅ Or use modern alternatives
const result = array.reduce((acc, item, i) => {
const chunkIndex = Math.floor(i / 3);
acc[chunkIndex] = [...(acc[chunkIndex] || []), item];
return acc;
}, []);
Lazy Load Heavy Dependencies¶
// Only load when needed
async function exportToPDF() {
const { jsPDF } = await import('jspdf');
const doc = new jsPDF();
// ...
}
Performance Monitoring¶
Web Vitals¶
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
Custom Metrics¶
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric);
// Send to analytics
if (metric.label === 'web-vital') {
analytics.track(metric.name, metric.value);
}
});
return null;
}
Quick Wins Checklist¶
- Enable React.StrictMode
- Use Next.js Image component
- Implement proper loading states
- Add Suspense boundaries
- Memoize expensive calculations
- Use Server Components by default
- Enable ISR for static content
- Lazy load heavy components
- Optimize images (WebP, sizing)
- Minimize client-side JavaScript
- Use font optimization
- Enable bundle analyzer
- Monitor Web Vitals
- Implement virtualization for long lists
Performance Targets¶
| Metric | Target | Good | Needs Improvement |
|---|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | < 2.5s | > 4.0s |
| FID (First Input Delay) | < 100ms | < 100ms | > 300ms |
| CLS (Cumulative Layout Shift) | < 0.1 | < 0.1 | > 0.25 |
| TTFB (Time to First Byte) | < 600ms | < 600ms | > 1.5s |
| Total Blocking Time | < 200ms | < 300ms | > 600ms |