Use when React performance optimization including memoization, lazy loading, and virtualization. Use when optimizing React applications.
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
name: react-performance description: Use when React performance optimization including memoization, lazy loading, and virtualization. Use when optimizing React applications. allowed-tools:
Master react performance optimization for building high-performance, scalable React applications with industry best practices.
React.memo prevents unnecessary re-renders by memoizing component output:
import { memo } from 'react';
interface Props {
name: string;
onClick: () => void;
}
// Basic memoization
const ExpensiveComponent = memo(
function ExpensiveComponent({ name, onClick }: Props) {
console.log('Rendering ExpensiveComponent');
return <button onClick={onClick}>{name}</button>;
});
// Custom comparison function
const CustomMemo = memo(
function Component({ user }: { user: User }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if passing nextProps would return the same result as prevProps
return prevProps.user.id === nextProps.user.id;
}
);
// When to use custom comparison
const ProductCard = memo(
function ProductCard({ product }: { product: Product }) {
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
},
(prev, next) => {
// Only re-render if these specific fields change
return (
prev.product.id === next.product.id &&
prev.product.name === next.product.name &&
prev.product.price === next.product.price
);
}
);
import { useMemo, useState } from 'react';
function DataTable({ items }: { items: Item[] }) {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// Expensive filtering and sorting
const processedItems = useMemo(() => {
console.log('Computing filtered and sorted items');
return items
.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
}, [items, filter, sortBy]);
// Expensive aggregate calculation
const statistics = useMemo(() => {
console.log('Computing statistics');
return {
total: processedItems.reduce((sum, item) => sum + item.price, 0),
average: processedItems.length
? processedItems.reduce((sum, item) => sum + item.price, 0) / processedItems.length
: 0,
count: processedItems.length
};
}, [processedItems]);
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<div>Total: ${statistics.total}</div>
<div>Average: ${statistics.average.toFixed(2)}</div>
<div>Count: {statistics.count}</div>
{processedItems.map(item => (
<div key={item.id}>{item.name} - ${item.price}</div>
))}
</>
);
}
import { useCallback, useState, memo } from 'react';
// Child component that only re-renders when necessary
const ListItem = memo(function ListItem({
item,
onDelete
}: {
item: Item;
onDelete: (id: string) => void;
}) {
console.log('Rendering ListItem', item.id);
return (
<div>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
);
});
function OptimizedList({ items }: { items: Item[] }) {
const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set());
// Without useCallback, this creates a new function on every render
// causing ListItem to re-render even with memo
const handleDelete = useCallback((id: string) => {
setDeletedIds(prev => new Set([...prev, id]));
// API call to delete
api.deleteItem(id);
}, []); // Empty deps means function never changes
const handleDeleteWithDeps = useCallback((id: string) => {
console.log('Already deleted:', deletedIds.size);
setDeletedIds(prev => new Set([...prev, id]));
}, [deletedIds]); // Re-create when deletedIds changes
const visibleItems = items.filter(item => !deletedIds.has(item.id));
return (
<>
{visibleItems.map(item => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</>
);
}
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
// Fallback component
function LoadingSpinner() {
return <div className="spinner">Loading...</div>;
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
// Preload on hover for better UX
function Navigation() {
const preloadDashboard = () => import('./pages/Dashboard');
const preloadProfile = () => import('./pages/Profile');
return (
<nav>
<a href="/dashboard" onMouseEnter={preloadDashboard}>Dashboard</a>
<a href="/profile" onMouseEnter={preloadProfile}>Profile</a>
</nav>
);
}
// Nested Suspense boundaries
function DashboardLayout() {
const Header = lazy(() => import('./components/Header'));
const Sidebar = lazy(() => import('./components/Sidebar'));
const Content = lazy(() => import('./components/Content'));
return (
<div className="dashboard">
<Suspense fallback={<div>Loading header...</div>}>
<Header />
</Suspense>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div>Loading content...</div>}>
<Content />
</Suspense>
</div>
);
}
import { FixedSizeList, VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
// Fixed size items
function VirtualList({ items }: { items: string[] }) {
const Row = ({ index, style }: {
index: number;
style: React.CSSProperties
}) => (
<div style={style} className="list-item">
{items[index]}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Variable size items
function VariableList({ items }: { items: Post[] }) {
const getItemSize = (index: number) => {
// Calculate height based on content
return items[index].content.length > 100 ? 120 : 80;
};
const Row = ({ index, style }: any) => (
<div style={style} className="post">
<h3>{items[index].title}</h3>
<p>{items[index].content}</p>
</div>
);
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
// With AutoSizer for responsive layouts
function ResponsiveList({ items }: { items: Item[] }) {
return (
<div style={{ height: '100vh', width: '100%' }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
itemCount={items.length}
itemSize={50}
width={width}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
}
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (first render) or "update" (re-render)
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) => {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
// Send to analytics
if (actualDuration > 100) {
analytics.track('slow-render', {
component: id,
duration: actualDuration,
phase
});
}
};
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}
// Nested profilers for granular monitoring
function Dashboard() {
return (
<div>
<Profiler id="Sidebar" onRender={onRenderCallback}>
<Sidebar />
</Profiler>
<Profiler id="Content" onRender={onRenderCallback}>
<Content />
</Profiler>
</div>
);
}
// Use dynamic imports for large libraries
function ChartComponent() {
const [Chart, setChart] = useState<any>(null);
useEffect(() => {
// Only load chart library when needed
import('chart.js').then(module => {
setChart(() => module.Chart);
});
}, []);
if (!Chart) return <div>Loading chart...</div>;
return <Chart data={data} />;
}
// Tree-shakeable imports
// GOOD: Import only what you need
import { format } from 'date-fns';
// BAD: Imports entire library
import moment from 'moment';
// Use webpack magic comments for chunk names
const AdminPanel = lazy(() =>
import(/* webpackChunkName: "admin" */ './AdminPanel')
);
const UserDashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './UserDashboard')
);
import { useState, useEffect } from 'react';
// Lazy loading images
function LazyImage({ src, alt, placeholder }: {
src: string;
alt: string;
placeholder?: string;
}) {
const [imageSrc, setImageSrc] = useState(placeholder || '');
const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
useEffect(() => {
if (!imageRef) return;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(imageRef);
}
});
});
observer.observe(imageRef);
return () => {
if (imageRef) observer.unobserve(imageRef);
};
}, [imageRef, src]);
return (
<img
ref={setImageRef}
src={imageSrc}
alt={alt}
loading="lazy"
/>
);
}
// Progressive image loading
function ProgressiveImage({ src, placeholder }: {
src: string;
placeholder: string;
}) {
const [currentSrc, setCurrentSrc] = useState(placeholder);
const [loading, setLoading] = useState(true);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setCurrentSrc(src);
setLoading(false);
};
}, [src]);
return (
<img
src={currentSrc}
style={{
filter: loading ? 'blur(10px)' : 'none',
transition: 'filter 0.3s'
}}
/>
);
}
import { useState, useTransition, useDeferredValue } from 'react';
// useTransition for non-urgent updates
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
setQuery(value); // Urgent: update input immediately
// Non-urgent: defer expensive search
startTransition(() => {
const searchResults = performExpensiveSearch(value);
setResults(searchResults);
});
};
return (
<>
<input
value={query}
onChange={e => handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending && <div>Searching...</div>}
<ResultsList results={results} />
</>
);
}
// useDeferredValue for deferring expensive renders
function ProductList({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// This filters with the deferred value
// UI stays responsive while filtering
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [products, deferredQuery]);
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Filter products..."
/>
{query !== deferredQuery && <div>Updating...</div>}
<div>
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</>
);
}
import { useState, useEffect, useCallback, useRef } from 'react';
// Custom debounce hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage with search
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
// Only search after user stops typing for 500ms
performSearch(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
// Custom throttle hook
function useThrottle<T>(value: T, limit: number): T {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
}, limit - (Date.now() - lastRan.current));
return () => clearTimeout(handler);
}, [value, limit]);
return throttledValue;
}
// Throttle scroll events
function InfiniteScroll() {
const [scrollPosition, setScrollPosition] = useState(0);
const throttledScroll = useThrottle(scrollPosition, 200);
useEffect(() => {
const handleScroll = () => setScrollPosition(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
// Only process scroll every 200ms
console.log('Throttled scroll position:', throttledScroll);
}, [throttledScroll]);
return <div>Scroll position: {throttledScroll}</div>;
}
import { createContext, useContext, useState, useMemo, ReactNode } from 'react';
// Split context to prevent unnecessary re-renders
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }: { children: ReactNode }) {
const [state, setState] = useState<State>(initialState);
// Memoize dispatch to keep it stable
const dispatch = useMemo(
() => ({
updateUser: (user: User) => setState(s => ({ ...s, user })),
updateSettings: (settings: Settings) => setState(s => ({ ...s, settings }))
}),
[]
);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// Components only re-render when they use state that actually changes
function UserProfile() {
const state = useContext(StateContext); // Re-renders on any state change
return <div>{state?.user.name}</div>;
}
function SettingsButton() {
const dispatch = useContext(DispatchContext); // Never re-renders
return <button onClick={() => dispatch?.updateSettings({})}>Settings</button>;
}
import { useEffect, useState } from 'react';
// worker.ts
// self.addEventListener('message', (e) => {
// const result = performHeavyCalculation(e.data);
// self.postMessage(result);
// });
function useWebWorker<T, R>(workerFn: (data: T) => R) {
const [result, setResult] = useState<R | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const execute = (data: T) => {
setLoading(true);
setError(null);
const worker = new Worker(
URL.createObjectURL(
new Blob([`(${workerFn.toString()})()`], { type: 'application/javascript' })
)
);
worker.postMessage(data);
worker.onmessage = (e) => {
setResult(e.data);
setLoading(false);
worker.terminate();
};
worker.onerror = (e) => {
setError(new Error(e.message));
setLoading(false);
worker.terminate();
};
};
return { result, error, loading, execute };
}
// Usage
function DataProcessor() {
const { result, loading, execute } = useWebWorker(
(data: number[]) => {
// Heavy calculation runs in worker
return data.reduce((sum, n) => sum + n * n, 0);
}
);
const handleProcess = () => {
execute(Array.from({ length: 1000000 }, (_, i) => i));
};
return (
<div>
<button onClick={handleProcess} disabled={loading}>
Process Data
</button>
{loading && <div>Processing...</div>}
{result && <div>Result: {result}</div>}
</div>
);
}
Use react-performance when you need to:
Profile before optimizing - Use React DevTools Profiler to identify actual bottlenecks before applying optimizations.
Use React.memo wisely - Only memoize components that render often with the same props or have expensive render logic.
Memoize callbacks and values - Use useCallback for functions passed to memoized children, useMemo for expensive computations.
Code split by route - Lazy load route components to reduce initial bundle size and improve load time.
Virtualize long lists - Use react-window or react-virtualized for lists with more than 100 items.
Optimize images - Lazy load images, use appropriate formats (WebP), implement progressive loading.
Debounce expensive operations - Debounce search inputs, API calls, and other expensive operations.
Split context strategically - Separate read and write contexts to prevent unnecessary consumer re-renders.
Monitor bundle size - Use webpack-bundle-analyzer to identify and remove large dependencies.
Use concurrent features - Leverage useTransition and useDeferredValue for better perceived performance.
Premature optimization - Don't optimize without measuring. Profile first, then optimize bottlenecks.
Overusing memo - Memoizing everything adds overhead. Only memoize when there's a measurable benefit.
Wrong dependencies - Missing dependencies in useMemo/useCallback leads to stale closures and bugs.
Not measuring impact - Always measure performance improvements with React Profiler or browser tools.
Ignoring bundle size - Importing large libraries for small features significantly impacts load time.
Memoizing primitives - useMemo is unnecessary for primitive values or simple calculations.
Not using key prop - Missing or incorrect keys in lists cause unnecessary re-renders and bugs.
Inline function definitions - Creating functions inline in JSX prevents React.memo from working effectively.
Not code splitting - Loading entire app upfront increases initial load time dramatically.
Forgetting about network - Optimize data fetching, use pagination, implement proper caching strategies.