Performance
When to Use
When a design system component is causing measurable performance issues, or when designing components that appear in long lists, frequently updating UIs, or high-traffic render paths.
Decision
| If you need... | Use... | Why |
|---|---|---|
| Skip re-render when props unchanged | React.memo |
Wraps component; skips render if all props shallowly equal |
| Skip expensive class computation | useMemo for cn() calls |
Only beneficial if CVA/cn computation is genuinely expensive (it rarely is) |
| Skip expensive derived values | useMemo with dependency array |
Memoize filtered/sorted arrays, not primitive calculations |
| Load component only when needed | React.lazy + Suspense |
Reduces initial bundle; appropriate for heavy editors, charts, modals |
| Stop re-render caused by new function ref | useCallback |
Memoizes event handlers passed to React.memo children |
| Let React Compiler handle it | No manual memos | React Compiler (released for React 17+, optimized for 19) auto-memoizes pure components; enable via babel/SWC plugin |
Pattern
When to add React.memo (with a rule):
// BEFORE adding React.memo, verify with React DevTools Profiler:
// 1. Component re-renders when parent re-renders with unchanged props
// 2. The re-render is visually noticeable OR in a hot path (long list)
const BadgeCount = React.memo(function BadgeCount({ count }: { count: number }) {
return <span className="rounded-full bg-primary px-2 text-xs">{count}</span>;
});
// Use when: Badge appears in a list of 100+ items that frequently updates
// Skip when: Badge appears once or twice; memo overhead > render cost
Lazy loading for heavy design system components:
// Heavy components (rich text editor, chart, code block) should be lazy
const RichTextEditor = React.lazy(() => import('./RichTextEditor'));
function EditorPage() {
return (
<React.Suspense fallback={<div className="h-48 animate-pulse bg-muted rounded" />}>
<RichTextEditor />
</React.Suspense>
);
}
Common Mistakes
- Adding
React.memoto every component "just in case" → shallow comparison has cost too; profile first; memoize only components with proven render waste useMemoforcn()calls →cn()is a string concatenation; it's faster than the memo overhead; don't memoize it- Passing new object/array literals as props to memoized components →
<Badge style={{ color: 'red' }}>creates new object on every render, defeatingReact.memo - Using
useCallbackwithoutReact.memoon the child →useCallbackonly helps when the receiving component is memoized; otherwise it's overhead with no benefit - Not profiling before optimizing → React DevTools Profiler shows exactly what re-renders and why; don't guess
- Over-splitting components for "performance" → component splitting has its own overhead (more reconciliation nodes); split for code organization, not performance
See Also
- Layout Components
- Testing
- Reference: React memo
- Reference: Saeloun — memo vs useMemo
- Reference: React Docs — useMemo