Performance Optimization
Hook performance optimization covers measuring before optimizing, useMemo, useCallback, React.memo, and avoiding premature memoization.
React applications can become slow when components perform expensive calculations, create unnecessary work, or cause avoidable re-renders.
React Hooks provide several tools that can help optimize performance:
useMemouseCallbackuseRefuseReducer
Performance optimization is not about using these Hooks everywhere.
The goal is to identify expensive or unnecessary work and optimize it only when there is a meaningful benefit.
What Makes a React Application Slow?
Performance problems can come from several sources:
- expensive calculations
- unnecessary re-renders
- large component trees
- unstable object references
- unstable function references
- excessive Effects
- unnecessary state updates
- expensive list rendering
- large amounts of DOM
- slow external systems
A useful mental model is:
State / Props Change↓Component Render↓Calculations↓Child Components↓DOM Commit
Performance optimization tries to reduce unnecessary work in this pipeline.
Rendering Is Not Automatically a Problem
A React component rendering again does not necessarily mean there is a performance problem.
React is designed to re-render components when their state or relevant inputs change.
The important question is:
Is the render doing expensive or unnecessary work?
Do not optimize simply because you see a re-render.
Measure first when possible.
The Main Optimization Tools
React provides several tools for different problems.
| Tool | Primary Purpose |
|---|---|
useMemo | Cache an expensive calculation |
useCallback | Cache a function reference |
useRef | Hold mutable values without causing renders |
React.memo | Skip child rendering when props are unchanged |
useReducer | Structure complex state transitions |
These tools solve different problems.
useMemo
useMemo caches the result of a calculation.
const filteredProducts = useMemo(() => {return products.filter((product) =>product.name.includes(query));}, [products, query]);
React can reuse the previously calculated value when the dependencies have not changed.
When useMemo Helps
Consider:
const result = expensiveCalculation(data);
If expensiveCalculation is costly and the component renders frequently for unrelated reasons, the calculation may be repeated unnecessarily.
With:
const result = useMemo(() => {return expensiveCalculation(data);}, [data]);
the result can be reused while data remains unchanged.
useMemo Is a Performance Optimization
Do not use useMemo as general-purpose storage.
This:
const value = useMemo(() => {return calculateValue();}, []);
does not mean:
This value is permanently stored.
It means React may reuse the memoized calculation result when appropriate.
The dependency list determines when React needs to reconsider the calculation.
Avoid Memoizing Cheap Calculations
This is usually unnecessary:
const fullName = useMemo(() => {return `${firstName} ${lastName}`;}, [firstName, lastName]);
The calculation is extremely cheap.
Prefer:
const fullName = `${firstName} ${lastName}`;
Memoization itself has complexity and overhead.
Use it where it provides value.
useCallback
useCallback memoizes a function reference.
const handleDelete = useCallback((id) => {deleteItem(id);}, []);
This can be useful when function identity matters.
For example, when passing a callback to a memoized child:
const Child = React.memo(function Child({onDelete,}) {// ...});
Then:
<Child onDelete={handleDelete} />
can benefit from the stable callback reference.
Function Identity
Every time a component renders, a function created inside the component normally gets a new function identity.
Example:
function Parent() {const handleClick = () => {console.log("Clicked");};return <Child onClick={handleClick} />;}
Even if the function behaves the same, its reference is different between renders.
Conceptually:
Render 1handleClick → Function ARender 2handleClick → Function B
useCallback can preserve the reference:
const handleClick = useCallback(() => {console.log("Clicked");}, []);
Conceptually:
Render 1handleClick → Function ARender 2handleClick → Function A
as long as its dependencies remain unchanged.
useCallback Does Not Make Functions Execute Faster
This is an important distinction.
useCallback does not optimize the internal execution speed of:
function handleClick() {expensiveOperation();}
It helps preserve the function's identity.
Use it when stable references matter.
React.memo
React.memo can skip rendering a child when its props are considered unchanged.
Example:
const ProductItem = React.memo(function ProductItem({product,}) {return <li>{product.name}</li>;});
If the parent renders again but the product prop remains equivalent according to the memo comparison, React can skip rendering that child.
However, memoization is most effective when the props themselves remain stable.
useCallback + React.memo
These tools are often used together.
Parent:
function TodoList() {const [todos, setTodos] = useState([]);const handleDelete = useCallback((id) => {setTodos((current) =>current.filter((todo) => todo.id !== id));}, []);return (<TodoItemstodos={todos}onDelete={handleDelete}/>);}
Memoized child:
const TodoItems = React.memo(function TodoItems({todos,onDelete,}) {// ...});
The stable callback helps the memoized child avoid unnecessary prop changes caused only by a newly created function.
Functional Updates and Stable Callbacks
Functional state updates are often useful for creating callbacks with fewer dependencies.
Instead of:
const addTodo = useCallback(() => {setTodos([...todos,newTodo,]);}, [todos]);
you can use:
const addTodo = useCallback(() => {setTodos((current) => [...current,newTodo,]);}, [newTodo]);
The callback no longer needs to read the current todos variable.
This can simplify dependency management.
useRef for Performance
useRef can hold a mutable value without triggering a re-render when the value changes.
const renderCount = useRef(0);renderCount.current += 1;
Changing:
renderCount.current
does not cause React to render again.
This makes refs useful for:
- DOM references
- timer IDs
- previous values
- external instances
- mutable values that do not affect UI
Do not use refs as a replacement for state when the value needs to appear in the UI.
State vs Ref
Use state when:
Value changes↓UI should update
Use a ref when:
Value changes↓UI does not need to update
Example state:
const [count, setCount] = useState(0);
Example ref:
const timerId = useRef(null);
Avoid Unnecessary State
Unnecessary state can cause unnecessary renders.
For example:
const [fullName, setFullName] = useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);
Prefer:
const fullName = `${firstName} ${lastName}`;
This removes:
- a state variable
- an Effect
- a state update
- an extra render caused by that update
Simpler code is often faster code.
Derived Data
Derived values should normally be calculated from existing state and props.
Example:
const completedTodos = todos.filter((todo) => todo.completed);
If this calculation is cheap, calculate it directly.
If it is genuinely expensive:
const completedTodos = useMemo(() => {return todos.filter((todo) => todo.completed);}, [todos]);
The optimization should be justified by the cost of the calculation.
Avoid Unnecessary Effect Chains
Consider:
State A↓Effect↓State B↓Effect↓State C
This can produce multiple updates and make the application difficult to reason about.
Prefer deriving values:
State↓Calculation↓UI
or use explicit event/state transitions when a real state change is required.
Object Identity
JavaScript objects are compared by reference.
For example:
const userA = {name: "Asha",};const userB = {name: "Asha",};
Even though the contents look identical:
userA === userB
is:
false
This matters for React optimization.
If a parent creates a new object on every render:
<Profileuser={{name: "Asha",}}/>
the child receives a new object reference each time.
useMemo for Stable Object References
Sometimes you need a stable object reference.
For example:
const options = useMemo(() => {return {theme,compact,};}, [theme, compact]);
Now options can remain referentially stable while its dependencies remain unchanged.
This can matter when:
- passing options to memoized children
- using an object as a dependency
- integrating with external libraries
Do not memoize every object automatically.
Context and Performance
Context can cause consumers to re-render when the context value changes.
Consider:
<AuthContext.Providervalue={{user,login,logout,}}>{children}</AuthContext.Provider>
The object passed to value may be recreated on each provider render.
In some architectures, you may stabilize the value:
const value = useMemo(() => {return {user,login,logout,};}, [user, login, logout]);
Functions such as login and logout may themselves use useCallback when stable references are useful.
However, context performance should be evaluated based on actual component behavior rather than automatically memoizing everything.
Large Lists
Rendering large lists can become expensive.
For example:
{products.map((product) => (<ProductCardkey={product.id}product={product}/>))}
Potential strategies include:
- keeping list items simple
- avoiding unnecessary state in parents
- memoizing expensive item components when useful
- keeping props stable when it matters
- virtualization for very large lists
For extremely large lists, virtualization renders only the visible portion rather than creating every item in the DOM at once.
Stable Keys
Good keys help React correctly identify list items.
Prefer:
key={product.id}
Avoid using array indexes when the list can be reordered, inserted into, or filtered:
key={index}
Stable keys are primarily about correct reconciliation and state preservation, not simply performance.
Expensive Calculations
Consider:
function Dashboard({ transactions }) {const report = generateLargeFinancialReport(transactions);return <Report data={report} />;}
If generating the report is expensive and the component frequently re-renders for unrelated reasons, memoization may help:
const report = useMemo(() => {return generateLargeFinancialReport(transactions);}, [transactions]);
This is a reasonable use of useMemo because the calculation is potentially expensive.
Performance Measurement
Do not guess about performance.
Measure it.
Useful tools include:
- React DevTools Profiler
- browser Performance tools
- application-specific timing
- production performance monitoring
Ask:
What is slow?↓Why is it slow?↓How often does it happen?↓What optimization reduces the cost?↓Did measurement improve?
This is better than blindly adding memoization.
React DevTools Profiler
The React DevTools Profiler can help identify:
- components that render frequently
- expensive renders
- render durations
- components affected by updates
A useful workflow is:
1. Reproduce the slow interaction.2. Record the interaction.3. Identify expensive components.4. Determine why they render.5. Optimize the actual bottleneck.6. Measure again.
Performance Optimization Workflow
A disciplined optimization process looks like this:
Step 1: Identify the Problem
Example:
Typing in search box feels slow.
Step 2: Measure
Determine whether the bottleneck is:
- filtering
- rendering
- network activity
- DOM size
- unnecessary updates
Step 3: Form a Hypothesis
For example:
Filtering 50,000 products on every keystroke is expensive.
Step 4: Optimize
Possible solutions:
useMemo
or:
debouncingvirtualizationbetter data structures
depending on the actual bottleneck.
Step 5: Measure Again
Verify that the optimization helped.
Memoization Has a Cost
Memoization is not free.
A memoized calculation needs:
- dependency tracking
- cached value management
- additional code
- memory
- reasoning about dependencies
Therefore:
useMemo(() => 2 + 2, []);
is usually pointless.
Simply write:
const value = 2 + 2;
The optimization should be worth the complexity.
Dependency Arrays and Performance
Consider:
const result = useMemo(() => {return expensiveCalculation(data);}, [data]);
If data is recreated unnecessarily:
const data = [...originalData];
then the memoized calculation may run again.
Memoization works best when its dependencies are themselves stable when appropriate.
This is why performance optimization often involves the entire data flow, not just adding useMemo.
Avoid Mutating Memoized Values
Do not do:
const products = useMemo(() => {return createProducts();}, []);products.push(newProduct);
The memoized value should be treated as immutable from the component's perspective.
If you need a changed array, create a new one:
const updatedProducts = [...products,newProduct,];
Performance and Immutability
Immutability supports predictable rendering and comparison.
For example:
setUser((current) => ({...current,name: "Asha",}));
creates a new object.
This allows React and memoization strategies to distinguish changed references from unchanged references.
Avoid mutating state directly:
user.name = "Asha";
Avoid Creating Unnecessary Work During Render
Consider:
function ProductList({ products }) {const data = products.map((product) => {return {...product,formattedPrice: formatPrice(product.price),};});return <List data={data} />;}
If this transformation is expensive and the component renders frequently for unrelated reasons, you may consider:
const data = useMemo(() => {return products.map((product) => ({...product,formattedPrice: formatPrice(product.price),}));}, [products]);
But first determine whether the transformation is actually expensive.
Splitting Components
Component boundaries can improve performance.
Instead of one huge component:
Dashboard├── Header├── Sidebar├── Chart├── Table└── Activity
extract meaningful components:
<Header /><Sidebar /><Chart /><TransactionTable /><ActivityFeed />
This improves:
- maintainability
- readability
- state ownership
- optimization opportunities
A smaller component can sometimes avoid work that an unrelated section does not need.
Localize State
State placement affects re-render scope.
Suppose a modal only affects one section.
Keeping its state near the modal can avoid causing an entire application subtree to render unnecessarily.
Prefer:
Modal└── modal state
instead of unnecessarily storing:
App└── modal state
when many unrelated components depend on the parent.
A useful rule is:
Keep state as close as practical to the components that need it.
Avoid Lifting State Too Far
State should be lifted when multiple components need it.
But lifting state too high can cause a larger subtree to re-render.
For example:
App├── Header├── Dashboard├── Sidebar└── Footer
If only Dashboard needs rapidly changing state, placing that state in App may cause unrelated sections to participate in updates.
Prefer local state when sharing is unnecessary.
useReducer and Performance
useReducer is primarily a state-organization tool, not a direct performance optimization.
It can still help structure complex state updates.
For example:
const [state, dispatch] = useReducer(reducer,initialState);
The benefit is clearer transitions:
dispatch({type: "filterChanged",payload: "completed",});
Do not choose useReducer simply because you think it is faster than useState.
Choose it when the state model benefits from reducer-based transitions.
Custom Hooks and Performance
A Custom Hook can encapsulate optimization logic.
For example:
function useFilteredProducts(products,query) {return useMemo(() => {return products.filter((product) =>product.name.toLowerCase().includes(query.toLowerCase()));}, [products, query]);}
Now the optimization is reusable:
const filteredProducts =useFilteredProducts(products, query);
Custom Hooks should still avoid unnecessary memoization.
Debouncing vs Memoization
These solve different problems.
Memoization
Prevents repeating a calculation when inputs have not changed.
useMemo(...)
Debouncing
Delays work until input changes stop for a specified period.
For example:
User types↓R↓Re↓Rea↓React↓wait↓perform search
Debouncing is useful for:
- search
- autocomplete
- filtering
- validation
- resize-related work
Memoization and debouncing are not interchangeable.
Memoization vs Virtualization
These are also different.
Memoization
Reduces repeated calculations or renders.
Virtualization
Reduces the number of rendered DOM elements.
For a list with:
100,000 items
memoizing each item does not necessarily solve the cost of having 100,000 DOM nodes.
Virtualization may be more appropriate.
Lazy Initialization
State initialization can itself be expensive.
Instead of:
const [data, setData] = useState(expensiveInitialization());
the function may be evaluated before useState receives its argument.
For expensive initialization, use a lazy initializer:
const [data, setData] = useState(() =>expensiveInitialization());
This allows React to initialize the state using the function rather than evaluating the expensive call on every component invocation.
useReducer Lazy Initialization
useReducer also supports lazy initialization:
const [state, dispatch] = useReducer(reducer,initialArgument,init);
Example:
function init(initialCount) {return {count: initialCount,};}
Then:
const [state, dispatch] = useReducer(reducer,100,init);
This is useful when initial state construction is expensive or needs dedicated initialization logic.
Common Performance Mistakes
1. Memoizing Everything
Avoid:
useMemo(...)useCallback(...)
everywhere without evidence of a problem.
2. Using useMemo for Cheap Values
Do not optimize trivial calculations.
3. Using useCallback Everywhere
Stable function references are useful only when they matter.
4. Treating React.memo as a Guarantee
React.memo is an optimization, not a semantic requirement that a component will never render.
5. Storing Derived Values in State
This can introduce unnecessary renders and synchronization complexity.
6. Lifting State Too High
Unnecessary parent state can expand the scope of re-renders.
7. Ignoring Large Lists
Rendering thousands of DOM nodes can be expensive even when individual components are simple.
8. Ignoring Effects
Poorly designed Effects can cause:
- repeated requests
- subscription leaks
- update loops
- unnecessary state changes
9. Optimizing Without Measuring
The fastest-looking code is not always the fastest code.
Measure the actual bottleneck.
Best Practices
1. Measure Before Optimizing
Use profiling and browser performance tools.
2. Optimize Expensive Work
Focus on actual bottlenecks.
3. Keep State Local
Avoid unnecessarily broad re-render scopes.
4. Derive Values Instead of Storing Them
Prefer calculations during rendering when they are cheap.
5. Use useMemo for Expensive Calculations
Do not use it automatically.
6. Use useCallback When Function Identity Matters
Especially around memoized children or dependency relationships.
7. Use React.memo Selectively
Memoize components when it meaningfully reduces rendering work.
8. Keep Props Stable When Necessary
Stable references can make memoization effective.
9. Keep Effects Correct
Incorrect Effects can create much larger performance problems than missing memoization.
10. Consider List Virtualization
For very large lists, reducing DOM size can be more valuable than memoizing calculations.
11. Prefer Simpler Code
An optimization that makes the code significantly harder to understand should have a measurable benefit.
Practical Example: Optimized Product List
import {memo,useCallback,useMemo,useState,} from "react";const ProductItem = memo(function ProductItem({product,onSelect,}) {return (<li><span>{product.name}</span><buttononClick={() => onSelect(product.id)}>Select</button></li>);});function ProductList({ products }) {const [query, setQuery] = useState("");const [selectedId, setSelectedId] =useState(null);const filteredProducts = useMemo(() => {const normalizedQuery =query.toLowerCase();return products.filter((product) =>product.name.toLowerCase().includes(normalizedQuery));}, [products, query]);const handleSelect = useCallback((id) => {setSelectedId(id);}, []);return (<div><inputvalue={query}onChange={(event) =>setQuery(event.target.value)}placeholder="Search products"/><ul>{filteredProducts.map((product) => (<ProductItemkey={product.id}product={product}onSelect={handleSelect}/>))}</ul>{selectedId !== null && (<p>Selected: {selectedId}</p>)}</div>);}export default ProductList;
This example combines:
useMemofor filteringuseCallbackfor stable function identitymemofor child rendering optimization- local state
- stable list keys
The important point is that each optimization has a specific reason.
Practical Example: Avoiding Unnecessary State
Before:
function CartSummary({ items }) {const [total, setTotal] = useState(0);useEffect(() => {setTotal(items.reduce((sum, item) =>sum + item.price * item.quantity,0));}, [items]);return <p>Total: ₹{total}</p>;}
After:
function CartSummary({ items }) {const total = items.reduce((sum, item) =>sum + item.price * item.quantity,0);return <p>Total: ₹{total}</p>;}
If the calculation is inexpensive, the second version is better.
If the calculation becomes genuinely expensive:
const total = useMemo(() => {return items.reduce((sum, item) =>sum + item.price * item.quantity,0);}, [items]);
Practical Example: Stable Context Value
A provider might contain:
function AppProvider({ children }) {const [user, setUser] = useState(null);const login = useCallback((userData) => {setUser(userData);}, []);const logout = useCallback(() => {setUser(null);}, []);const value = useMemo(() => {return {user,login,logout,};}, [user, login, logout]);return (<AppContext.Provider value={value}>{children}</AppContext.Provider>);}
This can reduce unnecessary context value identity changes.
However, if profiling shows no meaningful issue, the simpler implementation may be preferable.
Performance Optimization Checklist
When a React component feels slow:
Rendering
- Is the component rendering more often than necessary?
- Is the rendered tree unnecessarily large?
- Can state be moved closer to where it is used?
Calculations
- Is an expensive calculation repeated?
- Can
useMemohelp?
Functions
- Are callback references causing memoized children to receive changed props?
- Would
useCallbackhelp?
Components
- Would
React.memomeaningfully reduce rendering?
Effects
- Is an Effect causing unnecessary state updates?
- Are requests or subscriptions being repeated?
- Is cleanup correct?
Lists
- Is the list extremely large?
- Would virtualization help?
Data
- Are objects and arrays being recreated unnecessarily?
- Are references stable where they need to be?
Measurement
- Have you profiled the actual bottleneck?
Performance Optimization Strategy
A good optimization strategy can be summarized as:
Keep the architecture simple↓Measure performance↓Identify bottleneck↓Optimize specific work↓Measure again↓Keep only useful optimization
Avoid:
Add useMemo everywhereAdd useCallback everywhereAdd React.memo everywhereHope it gets faster
Performance engineering should be evidence-driven.
Practice Exercises
Exercise 1: useMemo
Create a component that filters 10,000 products.
Use useMemo to avoid repeating the filtering calculation when unrelated state changes.
Exercise 2: useCallback
Create a parent component and memoized child.
Pass a callback from the parent to the child.
Observe how function identity changes between renders and then use useCallback to stabilize it.
Exercise 3: React.memo
Create a list of products.
Memoize the product item component and determine which updates cause the item to render.
Exercise 4: Derived State
Find a component that stores a value that can be calculated from existing state.
Remove the unnecessary state and Effect.
Exercise 5: State Localization
Create a dashboard with:
- counter
- search box
- sidebar
- footer
Keep the rapidly changing state as local as practical.
Observe the effect of moving state higher or lower in the tree.
Exercise 6: Large List
Render a list containing thousands of items.
Compare:
- normal rendering
- memoized items
- virtualized rendering
Identify which technique has the greatest impact.
Exercise 7: Performance Profiling
Use React DevTools Profiler to identify:
- a frequently rendered component
- an expensive calculation
- an unnecessary state update
Document what caused the performance problem and what optimization improved it.
Mini Project: Optimized Product Dashboard
Build a Product Dashboard containing:
SearchFiltersProduct ListProduct DetailsCart Summary
Requirements:
Search
- search products by name
- avoid unnecessary expensive filtering
- debounce if searching an external API
Filters
Support:
CategoryPrice rangeAvailability
Product List
- use stable keys
- keep item components focused
- consider memoization when justified
Product Details
Only update the relevant section when possible.
Cart Summary
Derive totals instead of storing redundant state.
Performance
Profile the application and identify at least one actual bottleneck before adding an optimization.
Possible architecture:
ProductDashboard│├── Search│├── Filters│├── ProductList│ └── ProductItem│├── ProductDetails│└── CartSummary
Interview questions & answers
Interview preparation
12 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is performance optimization in React?
BeginnerModel answer
It is the process of reducing unnecessary rendering, calculations, DOM work, and external operations so the application responds efficiently.
2Does useCallback make a function faster?
BeginnerModel answer
No.
It primarily helps with function identity and can prevent unnecessary work in consumers that care about reference equality.
3When should you use useMemo?
BeginnerModel answer
Use it when:- a calculation is expensive
- the component renders often
- the dependencies often remain unchanged
- profiling or reasoning indicates the optimization is useful
4Is useReducer a performance Hook?
BeginnerModel answer
Not primarily.useReduceris mainly for structuring complex state transitions. It may contribute indirectly to clearer state architecture, but it should not be chosen merely for performance.
Intermediate
5What does useMemo do?
IntermediateModel answer
useMemocaches the result of a calculation and can reuse it when its dependencies have not changed.6What is React.memo?
IntermediateModel answer
React.memois a component optimization that can skip re-rendering a component when its props are considered unchanged.7When should you use useCallback?
IntermediateModel answer
Use it when function identity matters, such as when passing callbacks to memoized children or when a stable function reference is useful for another dependency relationship.
8How can state placement affect performance?
IntermediateModel answer
State updates can cause a component and its descendants to render.
Keeping state closer to the components that need it can reduce the scope of updates.
Advanced
9What does useCallback do?
AdvancedModel answer
useCallbackcaches a function reference and can preserve its identity between renders while its dependencies remain unchanged.10Should every component use React.memo?
AdvancedModel answer
No.
Use it when it provides a measurable or meaningful benefit.
11What is the difference between useMemo and useCallback?
AdvancedModel answer
useMemo → memoizes a value → useCallback → memoizes a function referenceConceptually:useCallback(fn, deps)is closely related to:useMemo(() => fn, deps)butuseCallbackcommunicates the intent more clearly.12Why should you measure before optimizing?
AdvancedModel answer
Because optimization adds complexity and a change that seems helpful may have little or no benefit.
Profiling identifies the actual bottleneck.
Performance Cheat Sheet
Memoize expensive calculation
const result = useMemo(() => {return expensiveCalculation(data);}, [data]);
Memoize callback
const handleClick = useCallback(() => {doSomething();}, []);
Memoized component
const Child = memo(function Child(props) {return <div />;});
Keep state local
State↓Nearest practical owner↓Smaller update scope
Derive cheap values
const total = price * quantity;
Functional state update
setCount((current) => current + 1);
Large lists
Small list↓Normal renderingLarge list↓Consider memoizationVery large list↓Consider virtualization
Optimization workflow
Measure↓Identify↓Optimize↓Measure again