Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 27, 2026·11 min read
Re-render Budgets: Performance & Scalability in State Architecture
Performance in React is an architectural concern before it's a memoization problem. This article covers the full re-render budget: what triggers renders, when useMemo and useCallback earn their cost, how granular selectors prevent cascade, how custom hooks decouple business logic, and how code splitting improves Time to Interactive.
Technical Series
Frontend State Architecture
Part 6 of 8
Re-render Budgets: Performance & Scalability in State Architecture
The fourth and final pillar: data flow over data storage. The most performant React applications aren't the ones with the most
useMemo calls — they're the ones where the state architecture makes unnecessary renders structurally impossible before any memoization is applied.React.memo, useMemo, and useCallback are not performance solutions. They are suppressors of a symptom. The architectural cure is designing state boundaries, store shapes, and component decomposition so that the renders that do fire are the ones that need to fire — and only those.1. What Actually Causes a Re-render
A complete list:
| Trigger | Notes |
|---|---|
setState / dispatch | Own state changed |
| Props value changed | Shallow comparison — new object reference triggers re-render even if contents are identical |
useContext value changed | Every consumer re-renders when Provider value changes |
| Parent component re-rendered | Default behavior — children re-render unless wrapped in React.memo |
The fourth trigger surprises most developers. A parent re-render causes all children to re-render by default, regardless of whether their props changed. This is why
React.memo exists — but as the architectural question is: why is the parent re-rendering unnecessarily in the first place?
Expand
Mental Model Check
Profiling workflow: (1) Identify the component that re-renders too often using React DevTools Profiler. (2) Check what triggered the render — is it a state change, a prop change, or parent cascade? (3) Fix the source of the unnecessary trigger before reaching for memoization.
2. useMemo — When It Earns Its Cost
useMemo caches a computed value between renders. It has a real cost: memory allocation for the cached value, and comparison work on every render. It only earns that cost in two cases:Case 1: Genuinely Expensive Computation
typescript
Case 2: Stable Reference for a Memoized Child
typescript
Performance / Safety Warning
Wrapping every value in
useMemo is cargo-cult performance engineering. For primitive return values (strings, numbers, booleans), useMemo adds overhead, not performance. Profile first; memoize only the computations the profiler identifies as expensive.3. useCallback — Stable Function Identity
useCallback memoizes a function reference. The only reason to use it is when the function is passed as a prop to a React.memo-wrapped component and you want to prevent that child from re-rendering:typescript
The common mistake is memoizing the callback without memoizing the child:
typescript
4. Granular Selectors: The Correct Prevention
The most effective re-render prevention is granular store subscriptions — subscribing only to the slice of state that matters:
typescript
With Zustand's selector pattern, two sibling components can subscribe to different fields of the same store and update completely independently:
typescript
5. Context Splitting for Fine-Grained Renders
When using Context, split contexts by the rate of change of their values, not by logical domain:
tsx
A component that only needs
user and theme now never re-renders when a notification arrives.6. Custom Hooks: Business Logic Decoupling
Custom hooks are the primary mechanism for separating what state exists from how the UI renders it:
typescript
typescript
The hook
useProductActions can be tested with renderHook from React Testing Library without rendering any UI. The component can be tested by mocking the hook entirely.7. Code Splitting & Lazy Loading
State-heavy modules (rich text editors, charting libraries, PDF viewers) should not be bundled with the initial page load.
React.lazy defers loading until the component is actually needed:tsx
The
Suspense fallback renders immediately; the actual component chunk loads in parallel. This directly reduces Time to Interactive (TTI) — the browser has less JavaScript to parse and execute before the page is usable.Pro Tip & Optimization
In Next.js, use
next/dynamic for the same effect with SSR control:typescript
8. Design System Components as Contracts
A design system component (
<Button>, <Input>, <Modal>) is a published API. It should:- Accept only what it needs — no passthrough props for parent state.
- Return stable prop shapes — changes are breaking changes.
- Be
React.memo-wrapped by default — they render frequently and their props are typically stable references.
typescript
When the parent component re-renders,
Button only re-renders if onClick, disabled, variant, or children changed. Since onClick is often a useCallback-wrapped handler, and the others are usually stable, Button essentially never re-renders unless it has to.9. References
Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#React#Performance#useMemo#useCallback#Re-renders#Code Splitting#Architecture#Scalability