React Performance Isn’t About useMemo — It’s About Render Boundaries

4 min read Original article ↗

When a React application starts feeling slow, the reflex is almost universal.

Add useMemo.

Wrap callbacks with useCallback.

Throw memo() around suspicious child components.

A filtered list recalculates?

useMemo.

A function gets recreated?

useCallback.

A component rerenders unexpectedly?

memo.

The logic feels reasonable. The tools are built for optimization, the problem appears local, and the fix seems quick.

The issue is that this often mistakes the symptom for the disease.

Most React performance problems do not begin because someone forgot a hook.

They begin because the application has weak render boundaries.

A surprising amount of React optimization advice starts from a flawed assumption:

rerenders are bad.

They are not.

React was built around rerendering.

State changes.

React recalculates UI.

That is normal operation.

The real question is not:

“Why did this component rerender?”

The better question is:

“Why was this component involved in this update at all?”

Performance problems usually appear when updates become:

  • too wide

  • too frequent

  • too expensive

Artwork: Relativity
Author: M.C. Escher

One input field changes.

Half the page updates.

Every keystroke propagates through a heavy component tree.

Rendering triggers:

  • sorting

  • filtering

  • formatting

  • rebuilding large objects

  • recomputing derived state

The rerender itself is rarely the core problem.

The update scope is.

Perfectly fine:

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button
      onClick={() => setCount(c => c + 1)}
    >
      {count}
    </button>
  )
}

Rerenders happen.

Nothing is wrong.

Optimization would solve nothing here.

useMemo is useful.

But it is not architectural medicine.

A common mistake looks like this:

const filteredUsers = useMemo(
  () =>
    users.filter(
      user =>
        user.name.includes(query)
    ),
  [users, query]
)

This may be perfectly appropriate.

Or completely irrelevant.

The missing question is:

why is this calculation running so often?

If every keystroke causes:

  • table rebuilds

  • sidebar rerenders

  • chart updates

  • modal recalculations

then memoizing one filter does not solve the architecture.

It simply places a performance-shaped bandage over a broader design problem.

Artwork: The Treachery of Images
Author: René Magritte

One of the most common React performance failures is state lifted too high.

A single parent component ends up owning everything.

function DashboardPage() {

  const [search, setSearch] =
    useState('')

  const [selectedRow, setSelectedRow] =
    useState(null)

  const [modalOpen, setModalOpen] =
    useState(false)

  const [loading, setLoading] =
    useState(false)

  const [tableData, setTableData] =
    useState([])

  const [errors, setErrors] =
    useState({})
}

Individually, nothing seems unusual.

Collectively, it becomes dangerous.

Now every small state change flows through the same parent.

Search input changes?

Large subtree rerenders.

Modal opens?

Table participates.

Loading flag updates?

Entire layout recalculates.

Developers often respond by layering optimization:

  • memo

  • useCallback

  • useMemo

The app becomes slightly quieter.

The architecture stays equally tangled.

The problem was never missing memoization.

The problem was missing boundaries.

Good React optimization often begins with a surprisingly unglamorous move:

move state closer to where it actually belongs.

Bad:

Page
 ├── SearchBox
 ├── DataTable
 ├── Sidebar
 └── Modal

Everything depends on Page state.

Better:

Page
 ├── SearchSection
 │    └── search state
 │
 ├── DataTable
 │    └── row selection state
 │
 └── Modal
      └── visibility state

Now updates become localized.

Opening a modal does not rebuild unrelated UI.

Typing into search does not redraw the entire page.

Render boundaries become narrower.

React performs less work because less work exists.

Artwork: Broadway Boogie Woogie
Author: Piet Mondrian

Not all rendering work carries equal cost.

Changing button text?

Cheap.

Sorting 20,000 rows after every keystroke?

Not cheap.

This is where optimization becomes practical.

Before adding hooks, map the hot path.

Ask:

What happens after the user types?

What changes after clicking a row?

Which components receive new props?

Which calculations rerun?

Once the update flow becomes visible, solutions become clearer.

Sometimes:

separate input from results

Sometimes:

virtualize large lists

Sometimes:

move formatting server-side

Sometimes:

the problem is not React at all.

The browser simply received far too much data.

Bad:

function UsersTable({
  users,
  query
}) {

  const rows =
    users
      .filter(user =>
        user.name.includes(query)
      )
      .sort(sortUsers)
      .map(renderRow)

  return <>{rows}</>
}

Every keystroke:

  • filters

  • sorts

  • maps

Potentially across thousands of records.

Now useMemo becomes reasonable.

But only after understanding the workload.

React Compiler changes the conversation.

Automatic memoization becomes increasingly possible.

Less repetitive optimization.

Less manual wrapping.

That is useful.

But it does not eliminate architectural thinking.

The compiler cannot decide:

  • where state belongs

  • which component owns data

  • why an entire screen depends on one object

Automatic optimization cannot untangle poor separation.

If one component mixes:

  • forms

  • analytics

  • tables

  • business rules

  • server state

  • UI state

no compiler magically transforms that into maintainable architecture.

Tools are getting smarter.

Boundary design still belongs to developers.

Artwork: Composition VIII
Author: Wassily Kandinsky

Before typing another optimization hook, ask:

Only after these questions does optimization become honest.

Then:

useMemo makes sense.

useCallback makes sense.

memo makes sense.

Without architectural understanding, they become decorative complexity.

React applications rarely become slow because someone forgot useMemo.

Much more often, they become slow because:

too much UI depends on too much frequently changing state.

Memoization can help.

It cannot replace understanding:

  • render flow

  • ownership

  • state placement

  • update boundaries

Strong React developers do not merely know where to place useMemo.

They understand:

what changed, why it changed, and what actually deserves to update.

React optimization does not begin with a hook.

It begins with a question:

What should actually change after this interaction?

Once that answer becomes clear, memo becomes a tool.

Until then, it is just another layer wrapped around architectural debt.

Discussion about this post

Ready for more?