Understand React Performance
Understand React Performance
A teacher types into a large student filter and notices a delay. Adding every available optimization before measuring can complicate the code without identifying whether the delay comes from filtering, rendering thousands of rows or a slow request.
Performance work improves a measured user experience. Memoization reuses a previous result when its relevant inputs have not changed. It can avoid selected work, but it does not establish correctness or make every interaction fast.
Measure the task
Identify the operation and user-visible delay.
Recognize reuse
Understand useMemo’s input relationship.
Keep correctness first
Make the interface work without relying on its cache.
Follow the user’s interaction
Interaction
A teacher changes the mark threshold.
Calculation
Records are filtered for that threshold.
Render
The matching rows are displayed.
Find the cost before choosing a technique
A render calls component functions to describe UI; committing updates applies necessary changes to the browser. A component rendering again does not imply every DOM node is replaced. A slow report could involve calculation, many visible rows, layout work or network waiting, so those costs deserve different solutions.
useMemo can cache a calculation between renders when its dependencies are unchanged. The calculation must be pure and include its reactive inputs in the dependency list. React’s useMemo reference treats it as an optimization rather than a semantic storage guarantee.
Reuse a threshold calculation across an unrelated update
The three-record list is intentionally tiny, so this example demonstrates the dependency relationship rather than a speed improvement. The visible list depends on minimum. Toggle notice changes another state value; the threshold does not change, so the cached filtered result can be reused.
Show marks at least 40 changes minimum from 30 to 40. The calculation then needs a new result, leaving only Asha. students is a fixed module-level array here; if it were a prop or changing state, that input would also belong in the dependency list.
React · JavaScript with JSX
import React, { useMemo, useState } from 'react';
const students = [
{ id: '007', name: 'Maya', mark: 35 },
{ id: '009', name: 'Ravi', mark: 28 },
{ id: '011', name: 'Asha', mark: 42 }
];
export default function App() {
const [minimum, setMinimum] = useState(30);
const [notice, setNotice] = useState(false);
const visible = useMemo(() => students.filter(student => student.mark >= minimum), [minimum]);
return <main><h1>Marks report</h1>
<button onClick={() => setMinimum(40)}>Show marks at least 40</button>
<button onClick={() => setNotice(value => !value)}>Toggle notice</button>
{notice && <p>School office opens at nine</p>}
<ul>{visible.map(student => <li key={student.id}>{student.name}: {student.mark}</li>)}</ul>
</main>;
}Expected screen and behavior
Initially: Marks report, both buttons, Maya: 35 and Asha: 42.
The 40 threshold leaves Asha: 42; Toggle notice shows a separate office message.A cached value is not another state owner
React · the memoized filter
import React, { useMemo, useState } from 'react';
const students = [
{ id: '007', name: 'Maya', mark: 35 },
{ id: '009', name: 'Ravi', mark: 28 },
{ id: '011', name: 'Asha', mark: 42 }
];
export default function App() {
const [minimum, setMinimum] = useState(30);
const [notice, setNotice] = useState(false);
const visible = useMemo(() => students.filter(student => student.mark >= minimum), [minimum]);
return <main><h1>Marks report</h1>
<button onClick={() => setMinimum(40)}>Show marks at least 40</button>
<button onClick={() => setNotice(value => !value)}>Toggle notice</button>
{notice && <p>School office opens at nine</p>}
<ul>{visible.map(student => <li key={student.id}>{student.name}: {student.mark}</li>)}</ul>
</main>;
}visible is derived from students and minimum. useMemo supplies a cached version of that derivation when appropriate; it does not give visible a separate setter. If React recalculates it, the result should still be correct. This is why side effects such as saving attendance do not belong in the calculation.
React compares dependency values by identity. A freshly created object in the dependency list changes identity on each render even when its fields look equal. Prefer expressing the actual primitive inputs, or placing necessary object construction inside the calculation, before reaching for additional layers of memoization.
Use the profiler and a representative dataset
Describe the failing task precisely: typing a name with several thousand records, switching a report, or scrolling a list. Use the React DevTools Profiler to inspect component work and browser performance tools to inspect scripting and layout. Compare before and after using the same task and dataset.
A large list may need pagination or rendering only the visible range rather than caching one filter. Slow fetching may need a data-loading or caching strategy rather than useMemo. React Compiler, when adopted through a compatible setup, can automate selected memoization; it does not remove the need to measure user-visible behavior. React documents profiling measurements.
Worked practice: a correct version without memoization
Remove useMemo while preserving the result. The complete answer uses a direct filter. The screen and both interactions remain correct; only the reuse opportunity changes. This is a useful check that the cache is not being treated as required state.
For three students, the direct calculation is often clearer. Keep the simpler version unless measurement justifies another choice. Optimization should not turn a straightforward school rule into a dependency puzzle that future changes can break.
React · JavaScript with JSX
import React, { useState } from 'react';
const students = [
{ id: '007', name: 'Maya', mark: 35 },
{ id: '009', name: 'Ravi', mark: 28 },
{ id: '011', name: 'Asha', mark: 42 }
];
export default function App() {
const [minimum, setMinimum] = useState(30);
const [notice, setNotice] = useState(false);
const visible = students.filter(student => student.mark >= minimum);
return <main><h1>Marks report</h1>
<button onClick={() => setMinimum(40)}>Show marks at least 40</button>
<button onClick={() => setNotice(value => !value)}>Toggle notice</button>
{notice && <p>School office opens at nine</p>}
<ul>{visible.map(student => <li key={student.id}>{student.name}: {student.mark}</li>)}</ul>
</main>;
}Expected screen and behavior
Initially: Maya: 35 and Asha: 42.
The 40 threshold leaves Asha: 42, without a memoization cache.Avoid expensive fixes for unmeasured work
Memoization has its own costs and can make incorrect dependencies harder to see. Do not suppress a dependency warning to force reuse of a stale result. Mutating a record in place can also make identity-based comparisons miss meaningful changes; immutable updates remain important.
Development checks can run calculations more than once to reveal impurities, so development logs alone are not a production benchmark. Network timing and device speed also vary. Choose an improvement that helps the actual user task, and verify that the original behavior still works after the change.
Check the dependency
Apply the measurement rule
Make performance a measured behavior
Separate calculation, rendering and external waiting. Memoize only reusable pure work with complete dependencies, and keep results correct if recalculated. Measure representative interactions, then choose a targeted improvement rather than adding caches everywhere.
Compare prioritizing UI updates with data caching; each addresses a different kind of waiting.
Reaching the recap and completing the quizzes are separate milestones.