React performance optimization including memoization, code splitting, and lazy loading. Use when optimizing React applications.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers performance optimization techniques for React applications.
Use this skill when:
MEASURE FIRST - Profile before optimizing. Premature optimization leads to complexity without benefit.
Prevent re-renders when props haven't changed.
import { memo } from 'react';
interface ListItemProps {
item: Item;
onSelect: (id: string) => void;
}
// ✅ Memoize when:
// - Component renders often with same props
// - Component is expensive to render
// - Parent re-renders frequently
const ListItem = memo(function ListItem({
item,
onSelect,
}: ListItemProps): React.ReactElement {
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
});
// With custom comparison
const ListItem = memo(
function ListItem({ item }: ListItemProps): React.ReactElement {
return <li>{item.name}</li>;
},
(prevProps, nextProps) => {
return prevProps.item.id === nextProps.item.id;
}
);
Cache expensive computations.
import { useMemo } from 'react';
function UserList({ users, filter }: UserListProps): React.ReactElement {
// ✅ Use for expensive computations
const filteredUsers = useMemo(() => {
return users
.filter((user) => user.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name));
}, [users, filter]);
// ❌ Don't use for simple operations
// const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// Just use: const fullName = `${first} ${last}`;
return (
<ul>
{filteredUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Keep function references stable.
import { useCallback, memo } from 'react';
function Parent(): React.ReactElement {
const [count, setCount] = useState(0);
// ✅ Stable callback for memoized children
const handleClick = useCallback((id: string) => {
console.log(`Clicked: ${id}`);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<MemoizedList onItemClick={handleClick} />
</div>
);
}
const MemoizedList = memo(function MemoizedList({
onItemClick,
}: {
onItemClick: (id: string) => void;
}): React.ReactElement {
console.log('List rendered'); // Only logs once!
return <ul>...</ul>;
});
import { lazy, Suspense } from 'react';
// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
function App(): React.ReactElement {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}
// For named exports, create intermediate module
// Dashboard.lazy.ts
export { Dashboard as default } from './Dashboard';
// App.tsx
const Dashboard = lazy(() => import('./Dashboard.lazy'));
function NavLink({ to, children }: NavLinkProps): React.ReactElement {
const prefetch = (): void => {
if (to === '/dashboard') {
import('./pages/Dashboard');
} else if (to === '/settings') {
import('./pages/Settings');
}
};
return (
<Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
{children}
</Link>
);
}
For long lists, render only visible items.
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }: { items: Item[] }): React.ReactElement {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[500px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index]?.name}
</div>
))}
</div>
</div>
);
}
// ❌ Single state object causes full re-render
const [state, setState] = useState({
user: null,
theme: 'light',
notifications: [],
});
// ✅ Split state - components only re-render for their state
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [notifications, setNotifications] = useState<Notification[]>([]);
import { create } from 'zustand';
interface Store {
user: User | null;
theme: 'light' | 'dark';
setUser: (user: User) => void;
setTheme: (theme: 'light' | 'dark') => void;
}
const useStore = create<Store>((set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
}));
// ✅ Subscribe only to what you need
function UserName(): React.ReactElement {
const userName = useStore((state) => state.user?.name);
return <span>{userName}</span>;
}
function ThemeToggle(): React.ReactElement {
const theme = useStore((state) => state.theme);
const setTheme = useStore((state) => state.setTheme);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
function LazyImage({ src, alt }: { src: string; alt: string }): React.ReactElement {
return (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
/>
);
}
import Image from 'next/image';
function OptimizedImage(): React.ReactElement {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For LCP images
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// ❌ New object each render
function Component({ filters }: { filters: { page: number; limit: number } }): React.ReactElement {
useEffect(() => {
fetchData(filters);
}, [filters]); // Infinite loop!
}
// ✅ Extract primitive values
function Component({ filters }: { filters: { page: number; limit: number } }): React.ReactElement {
const { page, limit } = filters;
useEffect(() => {
fetchData({ page, limit });
}, [page, limit]);
}
// ❌ New object each render
<Child style={{ color: 'red' }} />
// ✅ Stable reference
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
// Or define outside component
const redStyle = { color: 'red' };
<Child style={redStyle} />
// In development only
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}