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.
/plugin marketplace add JosiahSiegel/claude-plugin-marketplace/plugin install react-master@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-virtualMaster authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.