| name | react-performance |
| description | Performance optimization for React web applications. Use when optimizing renders, implementing virtualization, memoizing components, or debugging performance issues. |
| source | https://www.skillsdirectory.com/skills/aiskillstore-react-performance |
React Performance (Web)
Problem Statement
React performance issues often stem from unnecessary re-renders, unoptimized lists, and expensive computations on the main thread. Understanding React's rendering behavior is key to building performant applications.
Pattern: Memoization
useMemo - Expensive Computations
const sortedAndFilteredItems = useMemo(() => {
return items
.filter((item) => item.active)
.sort((a, b) => b.score - a.score)
.slice(0, 100);
}, [items]);
const sortedAndFilteredItems = items
.filter((item) => item.active)
.sort((a, b) => b.score - a.score);
const userName = useMemo(() => user.name, [user.name]);
When to use useMemo:
- Array transformations (filter, sort, map chains)
- Object creation passed to memoized children
- Computations with O(n) or higher complexity
useCallback - Stable Function References
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
<MemoizedItem onClick={handleClick} />
const handleClick = useCallback((id: string) => {
doSomething(unstableObject);
}, [unstableObject]);
When to use useCallback:
- Callbacks passed to memoized children
- Callbacks in dependency arrays
- Event handlers that would cause child re-renders
Pattern: React.memo
const ItemCard = memo(function ItemCard({
item,
onSelect
}: Props) {
return (
<div onClick={() => onSelect(item.id)}>
<h3>{item.name}</h3>
<p>{item.price}</p>
</div>
);
});
const ItemCard = memo(
function ItemCard({ item, onSelect }: Props) {
},
(prevProps, nextProps) => {
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.price === nextProps.item.price
);
}
);
When to use React.memo:
- List item components
- Components receiving stable primitive props
- Components that render frequently but rarely change
When NOT to use:
- Components that always receive new props
- Simple components (overhead > benefit)
- Root-level pages
Pattern: List Virtualization
For long lists, render only visible items using react-window or react-virtualized.
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<ItemCard item={items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={80}
>
{Row}
</FixedSizeList>
);
}
import { VariableSizeList } from 'react-window';
function VariableList({ items }: { items: Item[] }) {
const getItemSize = (index: number) => {
return items[index].expanded ? 200 : 80;
};
return (
<VariableSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
When to virtualize:
- Lists with 100+ items
- Complex item components
- Scrollable containers with many children
Pattern: Zustand Selector Optimization
Problem: Selecting entire store causes re-render on any state change.
const store = useAppStore();
const { items, loading, filters, ... } = useAppStore();
const items = useAppStore((s) => s.items);
const loading = useAppStore((s) => s.loading);
import { useShallow } from 'zustand/react/shallow';
const { items, loading } = useAppStore(
useShallow((s) => ({
items: s.items,
loading: s.loading
}))
);
Pattern: Avoiding Re-Renders
Object/Array Stability
<ChildComponent style={{ padding: 10 }} />
<ChildComponent config={{ enabled: true }} />
const style = useMemo(() => ({ padding: 10 }), []);
const config = useMemo(() => ({ enabled: true }), []);
<ChildComponent style={style} />
<ChildComponent config={config} />
const style = { padding: 10 };
function Parent() {
return <ChildComponent style={style} />;
}
Children Stability
<Parent>
{() => <Child />}
</Parent>
const child = useMemo(() => <Child />, [deps]);
<Parent>{child}</Parent>
Pattern: Code Splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
const Dashboard = lazy(() =>
import('./pages/Dashboard').then(module => ({
default: module.Dashboard
}))
);
Pattern: Debouncing and Throttling
import { useMemo } from 'react';
import { debounce, throttle } from 'lodash-es';
function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
const debouncedSearch = useMemo(
() => debounce(onSearch, 300),
[onSearch]
);
return (
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
/>
);
}
function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
const throttledLoad = useMemo(
() => throttle(onLoadMore, 1000),
[onLoadMore]
);
useEffect(() => {
const handleScroll = () => {
if (nearBottom()) {
throttledLoad();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [throttledLoad]);
return <div>...</div>;
}
Pattern: Image Optimization
<img
src={imageUrl}
loading="lazy"
alt="Description"
/>
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" />
)}
</div>
);
}
import Image from 'next/image';
<Image
src={imageUrl}
alt="Description"
width={400}
height={300}
placeholder="blur"
blurDataURL={blurHash}
/>
Pattern: Web Workers for Heavy Computation
self.onmessage = (e: MessageEvent<{ data: number[] }>) => {
const result = heavyComputation(e.data.data);
self.postMessage(result);
};
function DataProcessor({ data }: { data: number[] }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.onmessage = (e) => {
setResult(e.data);
};
worker.postMessage({ data });
return () => worker.terminate();
}, [data]);
return result ? <Results data={result} /> : <Loading />;
}
Pattern: Detecting Re-Renders
React DevTools Profiler
- Open React DevTools
- Go to Profiler tab
- Click record, interact, stop
- Review "Flamegraph" for render times
- Look for components rendering unnecessarily
why-did-you-render
import React from "react";
if (process.env.NODE_ENV === "development") {
const whyDidYouRender = require("@welldone-software/why-did-you-render");
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
ItemCard.whyDidYouRender = true;
Console Logging
function ItemCard({ item }: Props) {
console.log("ItemCard render:", item.id);
}
Performance Checklist
Before shipping:
Common Issues
|
Issue
|
Solution
|
|
|
|
|
List scroll lag
|
Virtualize list, memoize items
|
|
Component re-renders too often
|
Check selector specificity, memoize props
|
|
Slow initial render
|
Code split, reduce bundle size
|
|
Memory growing
|
Check for event listener cleanup, state accumulation
|
|
UI freezes on interaction
|
Move computation to web worker or defer
|
Relationship to Other Skills
- react-zustand-patterns: Selector optimization patterns
- react-async-patterns: Proper async handling prevents re-render loops