Skip to main content

Performance

Avoiding Unnecessary Renders

Written by Published

The cheapest render is one that never happens. Moving state to where it is used solves more performance problems than memoisation, and adds nothing to maintain.

Move state down

State high in the tree re-renders everything below it. If only one small component uses a value, the state belongs in that component - the opposite of lifting state up, and just as important.

The same input, two placements

jsx

function Heavy({ label }) {
  const renders = React.useRef(0)
  renders.current++
  return <p>{label} rendered {renders.current} times</p>
}

function SearchBox() {
  // State lives here, so only this component re-renders as you type.
  const [text, setText] = React.useState("")
  return <input value={text} onChange={(e) => setText(e.target.value)} placeholder="type here" />
}

function App() {
  return (
    <div>
      <SearchBox />
      <Heavy label="Sibling" />
      <p style={{ color: "#64748b" }}>
        Type in the box — the sibling's count does not move.
      </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

Had the input's state lived in App, every keystroke would re-render Heavy too. Nothing was memoised; the state was simply put in the right place.

Pass children through

A component re-renders when its own state changes, but its children were created by the parent. If they did not change, React reuses them - so content passed as children escapes the re-render.

children survive the parent's state

jsx

function Expensive() {
  const renders = React.useRef(0)
  renders.current++
  return <p>Child rendered {renders.current} times</p>
}

function Wrapper({ children }) {
  const [count, setCount] = React.useState(0)
  return (
    <div style={{ border: "1px solid #cbd5e1", padding: 10, borderRadius: 8 }}>
      <button onClick={() => setCount(count + 1)}>Wrapper state: {count}</button>
      {children}
    </div>
  )
}

function App() {
  // Created by App, so Wrapper's own state changes do not recreate it.
  return (
    <Wrapper>
      <Expensive />
    </Wrapper>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

The wrapper's count changes on every click, yet the child's render count stays at one. No memo, no useCallback - only a change in who creates the element.

Other things that help more than memo

  • Split a large component so a frequently changing piece is its own component.
  • Do not put derived values in state - an extra setter is an extra render.
  • Use the updater form so a handler does not need the current value as a dependency.
  • Key long lists properly - a wrong key rebuilds rows instead of updating them.

Each of these removes work rather than caching it. That is why they are the first thing to reach for: there is no dependency array to keep correct afterwards.