# React Performance

> When to activate: React performance optimization, bundle size, code splitting, lazy loading, profiler, memoization, concurrent features

- Skill: `mattakushi432/react-performance` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/react-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/react-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/react-performance

---


# React Performance Patterns

## Profiling First
```tsx
// React DevTools Profiler — always profile before optimizing
// Enable in DevTools → Profiler → Record

// Code profiling with React Profiler API
import { Profiler, type ProfilerOnRenderCallback } from 'react'

const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
  if (actualDuration > 16) {  // > 1 frame
    console.warn(`Slow render: ${id} (${phase}) took ${actualDuration.toFixed(1)}ms`)
  }
}

<Profiler id="Dashboard" onRender={onRender}>
  <Dashboard />
</Profiler>
```

## Memoization

### When to use memo / useMemo / useCallback
```tsx
// memo: stable component with expensive render and stable-ish props
const ExpensiveChart = memo(function Chart({ data }: { data: DataPoint[] }) {
  // renders a big SVG chart
  return <svg>...</svg>
}, (prev, next) => prev.data === next.data)

// useMemo: CPU-expensive pure computation (not just "avoid re-render")
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.priority - b.priority),
  [items]  // only re-sort when items changes
)

// useCallback: stable function ref passed to memoized child or used as dep
const handleSelect = useCallback((id: string) => {
  setSelected(id)
  onSelect(id)
}, [onSelect])  // onSelect itself needs to be stable
```

### Anti-patterns
```tsx
// BAD: memoizing cheap computations (overhead > savings)
const name = useMemo(() => user.firstName + ' ' + user.lastName, [user])

// BAD: memo on component that receives new object literals every render
function Parent() {
  return <Child style={{ color: 'red' }} />  // new object each render
}

// GOOD: extract stable values
const style = { color: 'red' }  // module-level constant
function Parent() { return <Child style={style} /> }
```

## State Colocation
```tsx
// BAD: state too high — re-renders entire tree on every keystroke
function App() {
  const [search, setSearch] = useState('')  // re-renders App + all children
  return <><SearchBox value={search} onChange={setSearch} /><HeavyList /></>
}

// GOOD: colocate state with the components that need it
function SearchBox() {
  const [search, setSearch] = useState('')  // only SearchBox re-renders
  return <input value={search} onChange={e => setSearch(e.target.value)} />
}
```

## Context Splitting
```tsx
// BAD: one context causes all consumers to re-render on any change
const AppContext = createContext({ user, theme, cart, notifications })

// GOOD: split contexts by change frequency
const UserContext    = createContext<User | null>(null)
const ThemeContext   = createContext<Theme>('light')
const CartContext    = createContext<Cart>({ items: [] })
```

## Lazy Loading
```tsx
import { lazy, Suspense, startTransition } from 'react'

const HeavyEditor = lazy(() => import('./HeavyEditor'))

function App() {
  const [showEditor, setShowEditor] = useState(false)

  // startTransition: mark as non-urgent (don't block urgent UI updates)
  const openEditor = () => startTransition(() => setShowEditor(true))

  return (
    <>
      <button onClick={openEditor}>Open Editor</button>
      {showEditor && (
        <Suspense fallback={<EditorSkeleton />}>
          <HeavyEditor />
        </Suspense>
      )}
    </>
  )
}
```

## Concurrent Features
```tsx
// useDeferredValue: defer non-urgent rendering
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query)
  const isStale = query !== deferredQuery

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <Suspense fallback={<Spinner />}>
        <Results query={deferredQuery} />
      </Suspense>
    </div>
  )
}

// useTransition: mark state update as non-urgent
function TabBar() {
  const [isPending, startTransition] = useTransition()
  const [tab, setTab] = useState('home')

  return (
    <>
      {tabs.map(t => (
        <button
          key={t}
          onClick={() => startTransition(() => setTab(t))}
          disabled={isPending}
        >
          {t}
        </button>
      ))}
      <Suspense fallback={<Spinner />}>
        <TabContent tab={tab} />
      </Suspense>
    </>
  )
}
```

## Bundle Optimization
```tsx
// Dynamic import for conditional heavy deps
async function exportToPDF() {
  const { jsPDF } = await import('jspdf')  // loaded only when needed
  const doc = new jsPDF()
  doc.save('report.pdf')
}

// Tree-shaking: named imports from specific sub-paths
import { format } from 'date-fns/format'           // not 'date-fns'
import { debounce } from 'lodash-es/debounce'       // not 'lodash'
```

## Image & Asset Performance
```tsx
// next/image with blur placeholder
import Image from 'next/image'

<Image
  src={product.imageUrl}
  alt={product.name}
  width={400}
  height={300}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
  placeholder="blur"
  blurDataURL={product.blurHash}
/>
```

## Keys & Reconciliation
```tsx
// BAD: index as key causes incorrect reconciliation on reorder/insert
{items.map((item, i) => <Item key={i} item={item} />)}

// GOOD: stable unique ID
{items.map(item => <Item key={item.id} item={item} />)}

// Intentional reset via key (new key = new component instance)
<ExpensiveForm key={userId} userId={userId} />
```

