React Performance Isn’t About useMemo — It’s About Render Boundaries
Why slow React applications usually come from state placement, update scope, and architectural coupling - not missing memoization hooks.
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.
Rerenders Are Not the Enemy
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
Too wide
One input field changes.
Half the page updates.
Too frequent
Every keystroke propagates through a heavy component tree.
Too expensive
Rendering triggers:
sorting
filtering
formatting
rebuilding large objects
recomputing derived state
The rerender itself is rarely the core problem.
The update scope is.
Example: Healthy rerender
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.
The useMemo Misconception
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
The Real Problem: State With No Boundaries
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:
memouseCallbackuseMemo
The app becomes slightly quieter.
The architecture stays equally tangled.
The problem was never missing memoization.
The problem was missing boundaries.
State Should Live Close to Ownership
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 stateNow 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
Expensive Work Belongs Off the Hot Path
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.
Example: Expensive hot path
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 Will Not Fix Architecture
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
A Practical Checklist Before Reaching for useMemo
Before typing another optimization hook, ask:
What actually changed?
Why did that update reach this component?
Can state move lower?
Can static and interactive parts be separated?
Are oversized objects flowing through props?
Is heavy work happening on the user’s hot path?
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.
Conclusion
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.






