Complete React performance optimization system. PROACTIVELY activate for: (1) React.memo and memoization, (2) useMemo and useCallback usage, (3) Code splitting with React.lazy, (4) List virtualization (react-window, react-virtuoso), (5) Avoiding unnecessary re-renders, (6) useTransition and useDeferredValue, (7) Bundle optimization, (8) Web Vitals and profiling. Provides: Profiler setup, memoization patterns, lazy loading, virtualization config, state colocation. Ensures optimal React performance with measurable improvements.
Optimizes React performance with memoization, code splitting, virtualization, and concurrent features.
npx claudepluginhub josiahsiegel/claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/virtualization-guide.md| Issue | Solution |
|---|---|
| Unnecessary re-renders | React.memo, useMemo, useCallback |
| Expensive computations | useMemo |
| Large bundles | Code splitting, React.lazy |
| Long lists | Virtualization (react-window) |
| Slow state updates | useTransition, useDeferredValue |
| State too high | State colocation |
| Tool | Purpose |
|---|---|
| React DevTools Profiler | Component render timing |
web-vitals | CLS, INP, LCP, FCP, TTFB |
| Bundle analyzer | Identify large dependencies |
Use for React performance optimization:
For general hooks: see react-hooks-complete
// Wrap components with Profiler for measurement
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log({
id,
phase,
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime,
commitTime,
});
};
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
function reportWebVitals(metric: Metric) {
console.log(metric);
// Send to analytics
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric),
});
}
onCLS(reportWebVitals); // Cumulative Layout Shift
onINP(reportWebVitals); // Interaction to Next Paint
onLCP(reportWebVitals); // Largest Contentful Paint
onFCP(reportWebVitals); // First Contentful Paint
onTTFB(reportWebVitals); // Time to First Byte
import { memo, useState } from 'react';
// Memoized component - only re-renders when props change
const ExpensiveList = memo(function ExpensiveList({
items,
onItemClick,
}: {
items: Item[];
onItemClick: (id: string) => void;
}) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// With custom comparison
const DeepCompareList = memo(
function DeepCompareList({ data }: { data: ComplexData }) {
return <div>{/* render */}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
}
);
import { useMemo, useState } from 'react';
function DataTable({ data, filters, sortConfig }: Props) {
// Memoize filtered data
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter((item) => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return item[key]?.toLowerCase().includes(value.toLowerCase());
});
});
}, [data, filters]);
// Memoize sorted data
const sortedData = useMemo(() => {
console.log('Sorting data...');
if (!sortConfig.key) return filteredData;
return [...filteredData].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}, [filteredData, sortConfig]);
// Memoize statistics
const stats = useMemo(() => ({
total: sortedData.length,
average: sortedData.reduce((sum, item) => sum + item.value, 0) / sortedData.length,
max: Math.max(...sortedData.map((item) => item.value)),
}), [sortedData]);
return (
<div>
<Stats data={stats} />
<Table data={sortedData} />
</div>
);
}
import { useCallback, useState, memo } from 'react';
// Child component that should not re-render unnecessarily
const SearchInput = memo(function SearchInput({
onSearch,
}: {
onSearch: (query: string) => void;
}) {
console.log('SearchInput rendered');
return <input onChange={(e) => onSearch(e.target.value)} />;
});
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
// Stable callback reference
const handleSearch = useCallback((searchQuery: string) => {
setQuery(searchQuery);
// Perform search...
}, []);
// Without useCallback, SearchInput would re-render on every parent render
return (
<div>
<SearchInput onSearch={handleSearch} />
<ResultsList results={results} />
</div>
);
}
import { lazy, Suspense } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
// With named exports
const Chart = lazy(() =>
import('./components/Charts').then((module) => ({
default: module.PieChart,
}))
);
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Preload on hover
const DashboardPage = lazy(() => import('./pages/Dashboard'));
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const handleMouseEnter = () => {
// Preload component on hover
if (to === '/dashboard') {
import('./pages/Dashboard');
}
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}
import { lazy, Suspense, useState } from 'react';
// Heavy component loaded only when needed
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ImageEditor = lazy(() => import('./components/ImageEditor'));
function CreatePost() {
const [showEditor, setShowEditor] = useState(false);
const [showImageEditor, setShowImageEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>Open Editor</button>
{showEditor && (
<Suspense fallback={<EditorSkeleton />}>
<RichTextEditor />
</Suspense>
)}
<button onClick={() => setShowImageEditor(true)}>Edit Image</button>
{showImageEditor && (
<Suspense fallback={<ImageEditorSkeleton />}>
<ImageEditor />
</Suspense>
)}
</div>
);
}
import { FixedSizeList, VariableSizeList } from 'react-window';
// Fixed size items
function VirtualizedList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Variable size items
function VirtualizedVariableList({ items }: { items: Item[] }) {
const getItemSize = (index: number) => {
return items[index].content.length > 100 ? 100 : 50;
};
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<h3>{items[index].title}</h3>
<p>{items[index].content}</p>
</div>
);
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
// Simple virtualized list
function VirtuosoList({ items }: { items: Item[] }) {
return (
<Virtuoso
style={{ height: '600px' }}
totalCount={items.length}
itemContent={(index) => (
<div className="item">
<h3>{items[index].title}</h3>
<p>{items[index].description}</p>
</div>
)}
/>
);
}
// With grouping
function GroupedList({ groups }: { groups: Group[] }) {
const groupCounts = groups.map((g) => g.items.length);
const allItems = groups.flatMap((g) => g.items);
return (
<Virtuoso
style={{ height: '600px' }}
groupCounts={groupCounts}
groupContent={(index) => (
<div className="group-header">{groups[index].name}</div>
)}
itemContent={(index) => (
<div className="item">{allItems[index].name}</div>
)}
/>
);
}
// Bad - state too high in tree
function App() {
const [searchQuery, setSearchQuery] = useState('');
return (
<div>
<Header /> {/* Re-renders when searchQuery changes */}
<SearchSection query={searchQuery} setQuery={setSearchQuery} />
<Footer /> {/* Re-renders when searchQuery changes */}
</div>
);
}
// Good - state colocated with usage
function App() {
return (
<div>
<Header />
<SearchSection /> {/* Manages its own state */}
<Footer />
</div>
);
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState('');
return (
<div>
<input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
<SearchResults query={searchQuery} />
</div>
);
}
// Bad - re-renders children on state change
function Modal({ isOpen, children }: Props) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return isOpen ? (
<div style={{ left: position.x, top: position.y }}>
{children} {/* Re-renders when position changes */}
</div>
) : null;
}
// Good - children are stable
function Modal({ isOpen, children }: Props) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// children is a prop, not re-created on render
return isOpen ? (
<div style={{ left: position.x, top: position.y }}>
{children}
</div>
) : null;
}
// Bad - entire tree re-renders
function SlowComponent({ isOpen }: { isOpen: boolean }) {
const [count, setCount] = useState(0);
const [items] = useState(generateLargeList());
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveTree items={items} /> {/* Re-renders on count change */}
</div>
);
}
// Good - expensive component isolated
function FastComponent() {
return (
<div>
<Counter />
<ExpensiveTreeWrapper />
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
function ExpensiveTreeWrapper() {
const [items] = useState(generateLargeList());
return <ExpensiveTree items={items} />;
}
import { useState, useTransition, memo } from 'react';
const SlowList = memo(function SlowList({ text }: { text: string }) {
const items = [];
for (let i = 0; i < 10000; i++) {
items.push(<li key={i}>{text}</li>);
}
return <ul>{items}</ul>;
});
function SearchWithTransition() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// Urgent update - keep input responsive
setQuery(value);
// Non-urgent update - can be interrupted
startTransition(() => {
setQuery(value);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<SlowList text={query} />
</div>
</div>
);
}
import { useState, useDeferredValue, memo } from 'react';
const SearchResults = memo(function SearchResults({ query }: { query: string }) {
// Expensive search/filter operation
const results = expensiveSearch(query);
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
);
});
function SearchWithDeferredValue() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Show stale indicator
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}
function LazyImage({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) {
return (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
{...props}
/>
);
}
// With placeholder
function OptimizedImage({ src, alt, width, height }: Props) {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div style={{ position: 'relative', width, height }}>
{!isLoaded && <div className="placeholder skeleton" />}
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
</div>
);
}
function ResponsiveImage({ src, alt }: { src: string; alt: string }) {
return (
<picture>
<source
media="(max-width: 640px)"
srcSet={`${src}?w=640 1x, ${src}?w=1280 2x`}
/>
<source
media="(max-width: 1024px)"
srcSet={`${src}?w=1024 1x, ${src}?w=2048 2x`}
/>
<img
src={`${src}?w=1920`}
alt={alt}
loading="lazy"
decoding="async"
/>
</picture>
);
}
# With webpack
npx webpack-bundle-analyzer stats.json
# With Vite
npx vite-bundle-analyzer
// Bad - imports entire library
import _ from 'lodash';
const sorted = _.sortBy(items, 'name');
// Good - import only what you need
import sortBy from 'lodash/sortBy';
const sorted = sortBy(items, 'name');
// Best - use native methods when possible
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
// Load heavy libraries on demand
async function handleExport() {
const { jsPDF } = await import('jspdf');
const doc = new jsPDF();
doc.text('Hello world!', 10, 10);
doc.save('document.pdf');
}
async function handleChart() {
const { Chart } = await import('chart.js/auto');
new Chart(ctx, config);
}
| Issue | Solution |
|---|---|
| Unnecessary re-renders | React.memo, useMemo, useCallback |
| Expensive computations | useMemo |
| Large bundles | Code splitting, lazy loading |
| Long lists | Virtualization |
| Slow state updates | useTransition, useDeferredValue |
| Image performance | Lazy loading, responsive images |
| State too high | State colocation |
For comprehensive guides on specific optimization techniques, see:
references/virtualization-guide.md - Complete guide to virtualized lists with react-window, react-virtuoso, and @tanstack/react-virtual