Claude
Skills
Sign in
Back

react-render-optimization

Included with Lifetime
$97 forever

Teaches React rendering performance optimization patterns. Use when reducing unnecessary re-renders, optimizing memoization, improving state design, or diagnosing React performance issues.

Design

What this skill does


# React Render Optimization

## Table of Contents

- [When to Use](#when-to-use)
- [Instructions](#instructions)
- [Details](#details)
- [Source](#source)

Practical patterns for eliminating unnecessary re-renders, reducing rendering cost, and keeping React UIs responsive. These patterns apply to any React application — whether you're using Vite, Next.js, Remix, or a custom setup.

## When to Use

Reference these patterns when:
- Components re-render more often than expected
- UI feels sluggish during typing, scrolling, or interactions
- Profiler shows wasted renders in the component tree
- Building performance-sensitive features (dashboards, editors, lists)
- Reviewing or refactoring existing React components

## Instructions

- Apply these patterns during code generation, review, and refactoring. When you see an anti-pattern, suggest the corrected version with an explanation.

## Details

### Overview

React re-renders a component whenever its state changes, a parent re-renders, or context it consumes updates. Most re-renders are harmless, but when they trigger expensive computation, deep trees, or layout thrashing they become visible to users.

The patterns below are ordered by impact — address the biggest wins first before reaching for micro-optimizations.

---

### 1. Compute Derived Values During Render — Don't Store Them

**Impact: HIGH** — Eliminates an entire category of bugs and unnecessary state.

Storing values that can be computed from existing state or props creates synchronization problems and extra re-renders. Compute them inline instead.

**Avoid — redundant state that drifts:**

```tsx
function ProductList({ products }: { products: Product[] }) {
  const [search, setSearch] = useState('')
  const [filtered, setFiltered] = useState(products)

  useEffect(() => {
    setFiltered(products.filter(p =>
      p.name.toLowerCase().includes(search.toLowerCase())
    ))
  }, [products, search])

  return (
    <>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </>
  )
}
```

**Prefer — derive during render (cheap derivations use plain `const`):**

```tsx
function ProductList({ products }: { products: Product[] }) {
  const [search, setSearch] = useState('')

  // Cheap derivation — plain const, no useMemo needed
  const hasSearch = search.length > 0
  const normalizedSearch = search.toLowerCase()

  // Expensive derivation — useMemo is justified when iterating large arrays
  const filtered = useMemo(
    () => products.filter(p =>
      p.name.toLowerCase().includes(normalizedSearch)
    ),
    [products, normalizedSearch]
  )

  return (
    <>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      {hasSearch && <ClearButton />}
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </>
  )
}
```

**When to use `useMemo` vs a plain `const`:**
- **Plain `const`** — boolean flags, string formatting, simple arithmetic, object property access, `.length` checks. These are essentially free and `useMemo` overhead is not worth it.
- **`useMemo`** — filtering/sorting arrays, building data structures, `JSON.parse`, expensive transformations, anything that iterates collections or involves O(n) work.

The rule: if the expression returns a primitive or is a single property access, skip `useMemo`. If it iterates or transforms data, wrap it.

> **React Compiler note:** If React Compiler is enabled, it auto-memoizes expressions and you can skip manual `useMemo` calls.

---

### 2. Subscribe to Coarse-Grained State, Not Raw Values

**Impact: HIGH** — Prevents re-renders on irrelevant changes.

If your component only cares about a derived boolean (e.g., "is mobile?"), don't subscribe to the raw value that changes continuously.

**Avoid — re-renders on every pixel:**

```tsx
function Sidebar() {
  const width = useWindowWidth() // fires on every resize
  const isMobile = width < 768
  return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}
```

**Prefer — re-renders only when the boolean flips:**

```tsx
function Sidebar() {
  const isMobile = useMediaQuery('(max-width: 767px)')
  return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}
```

This applies broadly: subscribe to `isLoggedIn` rather than the entire user object, `hasItems` rather than the full cart array, etc.

---

### 3. Extract Expensive Subtrees into Memoized Components

**Impact: HIGH** — Enables early returns and skip-rendering.

When a parent has fast paths (loading, error, empty), expensive children still compute if they live in the same component. Extract them so React can skip their render entirely.

**Avoid — avatar computation runs even during loading:**

```tsx
function Profile({ user, loading }: Props) {
  const avatar = useMemo(() => processAvatar(user), [user])

  if (loading) return <Skeleton />
  return <div><img src={avatar} /></div>
}
```

**Prefer — computation skipped when loading:**

```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
  const avatar = useMemo(() => processAvatar(user), [user])
  return <img src={avatar} />
})

function Profile({ user, loading }: Props) {
  if (loading) return <Skeleton />
  return <div><UserAvatar user={user} /></div>
}
```

> **React Compiler note:** The compiler auto-memoizes, making manual `memo()` wrapping less necessary. But extracting components for early returns is still valuable.

---

### 4. Use Lazy State Initialization

**Impact: MEDIUM** — Avoids wasted computation on every render.

When `useState` receives a function call as its initial value, that call executes on every render even though the result is only used once. Pass a function reference instead.

**Avoid — `buildIndex()` runs every render:**

```tsx
const [index, setIndex] = useState(buildSearchIndex(items))
```

**Prefer — runs only on mount:**

```tsx
const [index, setIndex] = useState(() => buildSearchIndex(items))
```

Use lazy init for: `JSON.parse`, `localStorage` reads, building data structures, heavy transformations. Skip it for simple primitives like `useState(0)` or `useState(false)`.

---

### 5. Use Functional setState for Stable Callbacks

**Impact: MEDIUM** — Removes state variables from dependency arrays.

When a callback only needs the previous state to compute the next state, use the functional form. This eliminates the state variable from the dependency array and produces a stable callback identity.

**Avoid — callback changes when `count` changes:**

```tsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(count + 1), [count])
```

**Prefer — callback is always stable:**

```tsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])
```

---

### 6. Put Interaction Logic in Event Handlers, Not Effects

**Impact: MEDIUM** — Avoids re-running side effects on dependency changes.

If a side effect is triggered by a user action (click, submit, drag), run it in the event handler. Modeling it as state + effect causes re-runs when unrelated dependencies change.

**Avoid — effect re-runs when `theme` changes:**

```tsx
function Form() {
  const [submitted, setSubmitted] = useState(false)
  const theme = useContext(ThemeContext)

  useEffect(() => {
    if (submitted) {
      post('/api/register')
      showToast('Registered', theme)
    }
  }, [submitted, theme])

  return <button onClick={() => setSubmitted(true)}>Submit</button>
}
```

**Prefer — logic in the handler:**

```tsx
function Form() {
  const theme = useContext(ThemeContext)

  function handleSubmit() {
    post('/api/register')
    showToast('Registered', theme)
  }

  return <button onClick={handleSubmit}>Submit</button>
}
```

---

### 7. Use `useRef` for Transient, High-Frequency Values

**Impact: MEDIUM** — Prevents re-renders on rapid updates.

Values that change very frequently (mouse position, scroll offset, interval ticks) but do
Files: 1
Size: 27.6 KB
Complexity: 36/100
Category: Design

Related in Design