# Performance Optimization

> Detect and fix React performance anti-patterns — unnecessary re-renders, missing memo, inline objects, large bundle size, lazy loading. Use in any React project.

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

---


# Performance Optimization Skill

## When to Use This Skill
- Components re-rendering too frequently
- App feels slow or laggy
- Large bundle size warnings
- Lists with many items performing poorly
- Before shipping to production

---

## Rule 1 — Wrap Expensive Components in React.memo

```jsx
// ❌ Re-renders whenever parent re-renders
function ExpensiveChart({ data, title }) {
  return <div>{/* expensive render */}</div>;
}

// ✅ Only re-renders when data or title changes
const ExpensiveChart = React.memo(function ExpensiveChart({ data, title }) {
  return <div>{/* expensive render */}</div>;
});

// ✅ With custom comparison for complex objects
const ExpensiveChart = React.memo(
  function ExpensiveChart({ data, title }) {
    return <div>{/* expensive render */}</div>;
  },
  (prevProps, nextProps) => prevProps.data.id === nextProps.data.id
);
```

**AI instruction:** Any component that receives props and does significant work SHOULD be wrapped in React.memo.

---

## Rule 2 — Stable References with useCallback

```jsx
// ❌ New function reference on every render — breaks memo on child
function Parent() {
  const handleClick = () => console.log('clicked');
  return <MemoChild onClick={handleClick} />;
}

// ✅ Stable reference — memo actually works
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // deps: only recreate when these change

  return <MemoChild onClick={handleClick} />;
}
```

**AI instruction:** Event handlers passed as props MUST use useCallback when the receiving component uses React.memo.

---

## Rule 3 — Memoize Expensive Calculations

```jsx
// ❌ Recalculates on every render
function ProductList({ products, filters }) {
  const filtered = products
    .filter(p => filters.category === p.category)
    .sort((a, b) => b.price - a.price);

  return <List items={filtered} />;
}

// ✅ Only recalculates when products or filters change
function ProductList({ products, filters }) {
  const filtered = useMemo(() =>
    products
      .filter(p => filters.category === p.category)
      .sort((a, b) => b.price - a.price),
    [products, filters]
  );

  return <List items={filtered} />;
}
```

**AI instruction:** Any array/object derived from props that's used in JSX or passed as props SHOULD use useMemo if the calculation is non-trivial.

---

## Rule 4 — Never Create Objects/Arrays Inline in JSX

```jsx
// ❌ New object reference every render — breaks child memo
<Component style={{ margin: 0, padding: 0 }} options={['a', 'b']} />

// ✅ Move outside component (if static)
const STYLE = { margin: 0, padding: 0 };
const OPTIONS = ['a', 'b'];
<Component style={STYLE} options={OPTIONS} />

// ✅ Or useMemo if dynamic
const style = useMemo(() => ({ margin: 0, padding: count }), [count]);
```

---

## Rule 5 — Virtualize Long Lists

```jsx
// ❌ Renders all 10,000 items — kills performance
function HugeList({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

// ✅ Only renders visible items
import { FixedSizeList } from 'react-window';

function HugeList({ items }) {
  return (
    <FixedSizeList height={600} itemCount={items.length} itemSize={35}>
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </FixedSizeList>
  );
}
```

**AI instruction:** Lists with more than 100 items SHOULD use react-window or react-virtual.

---

## Rule 6 — Lazy Load Routes and Heavy Components

```jsx
// ❌ All routes loaded upfront — large initial bundle
import Dashboard from './Dashboard';
import Analytics from './Analytics';
import Reports from './Reports';

// ✅ Load only when needed
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Reports = lazy(() => import('./Reports'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}
```

---

## Rule 7 — Correct useEffect Dependencies

```jsx
// ❌ Missing deps — stale closure bug
useEffect(() => {
  fetchData(userId);
}, []); // userId never updates

// ❌ Too many deps — runs too often
useEffect(() => {
  fetchData(userId);
}, [userId, otherThing, anotherThing]);

// ✅ Only what the effect actually uses
useEffect(() => {
  fetchData(userId);
}, [userId]);
```

---

## Rule 8 — State Updates Batching

```jsx
// ❌ Two re-renders (React 17 and below in event handlers)
function handleClick() {
  setCount(c => c + 1);
  setName('updated');
}

// ✅ One re-render (React 18 auto-batches, but explicit is safer)
import { flushSync } from 'react-dom';
// Or just trust React 18 automatic batching
```

---

## Companion Script
```bash
npx reactforge performance ./src
npx reactforge detect-slow ./src
```

---

## Quick Checklist
- [ ] Components receiving props wrapped in React.memo
- [ ] Event handlers in props use useCallback
- [ ] Expensive calculations use useMemo
- [ ] No inline objects/arrays passed as props
- [ ] Lists > 100 items use virtualisation
- [ ] Routes use React.lazy + Suspense
- [ ] useEffect dependency arrays are correct and minimal

