React Performance Optimization

Reduce wasted renders with memoization and stable references

Overview

Most React performance issues come from unnecessary re-renders and recreating work on every render. The core tools are React.memo to skip re-rendering unchanged components, useMemo to cache values, and useCallback to keep function identities stable. Measure first with the Profiler—optimize only proven hot paths.

Syntax / Usage

Wrap a component in React.memo so it only re-renders when its props change by reference.

import { memo, useCallback, useState } from 'react'

type RowProps = { label: string; onSelect: (label: string) => void }

const Row = memo(function Row({ label, onSelect }: RowProps) {
  console.log('render', label)
  return <li onClick={() => onSelect(label)}>{label}</li>
})

function List({ items }: { items: string[] }) {
  const [selected, setSelected] = useState<string | null>(null)

  const handleSelect = useCallback((label: string) => {
    setSelected(label)
  }, [])

  return (
    <ul>
      {items.map((item) => (
        <Row key={item} label={item} onSelect={handleSelect} />
      ))}
    </ul>
  )
}

Examples

Memoize a derived value so a costly computation runs only when inputs change:

const total = useMemo(
  () => rows.reduce((sum, r) => sum + r.amount, 0),
  [rows]
)

Avoid inline objects as props to memoized children—they break reference equality:

const style = useMemo(() => ({ padding: 8, color }), [color])
return <Badge style={style} />

Common Mistakes

  • Wrapping everything in memo/useMemo without measuring—the overhead can exceed the savings
  • Passing new inline functions or objects to memoized components, defeating the memo
  • Using array index as a key, causing incorrect reuse and re-renders on reorder
  • Ignoring the React DevTools Profiler and guessing at bottlenecks
  • Storing large derived state instead of computing it during render

See Also

use-memo use-callback react-context-patterns