Measure component work before optimizing it.
Read the explanation, run the worked code, then complete the practice lab in Assignments. This advanced lesson includes worked code and a dedicated practice lab.
Understand the idea
Performance work starts with a reproducible interaction. Use React DevTools Profiler or the Profiler component to inspect commits and rendering cost. Rendering is only part of performance: network latency, layout, paint and bundle loading need their own measurements. Development timings and Strict Mode are useful for diagnosis but are not production benchmarks.
React.memo compares props to decide whether a parent update can skip rendering a child. useMemo can reuse a calculation and useCallback can retain a function identity. None should be necessary for correctness. A fresh object or function prop can defeat a shallow comparison even if it represents the same information. Memoized children can still update for their own state or context.
The example keeps a moderately large result list separate from an unrelated counter. A stable module-level array and a memoized list let you compare typing with an unrelated click. Small lists may show no meaningful benefit. If your build uses React Compiler, inspect the output and behavior before adding manual memoization everywhere. Prefer simple component boundaries and local state before caching.
Run the example
Use a separate React practice project with a Node.js version supported by Vite. Create it once with these commands, then use its development URL. Do not paste this into the Edupoly website’s own source files.
npm create vite@latest react-practice -- --template react
cd react-practice
npm install
npm run dev
Replace src/App.jsx with the following complete example. Keep Vite’s generated src/main.jsx entry point.
src/App.jsx
import { memo, Profiler, useMemo, useState } from 'react';
const students = Array.from({length: 2000}, (_, i) => ({id: i, name: `Student ${i}`}));
const Results = memo(function Results({query}) {
const rows = useMemo(() => students.filter(s => s.name.includes(query)), [query]);
return <ul>{rows.map(s => <li key={s.id}>{s.name}</li>)}</ul>;
});
function report(id, phase, actualDuration) {
console.log(id, phase, `${actualDuration.toFixed(2)}ms`);
}
export default function App() {
const [query, setQuery] = useState(''); const [count, setCount] = useState(0);
return <main><label>Search <input value={query} onChange={e => setQuery(e.target.value)} /></label><button onClick={() => setCount(n => n + 1)}>Unrelated count: {count}</button>
<Profiler id="Student results" onRender={report}><Results query={query} /></Profiler>
</main>;
}
Expected result
Typing filters the list. The unrelated counter changes without changing list content; compare measured work with and without memo instead of asserting a fixed timing.
Apply this to your assignments
Record a typing interaction and ten unrelated clicks. Compare before and after removing memo, using the same data and browser conditions. Add a callback prop, then inspect how a newly created callback affects the result. Report measurements and whether the optimization is worth keeping.
Practice starting point
Use the worked example above as your starting point.
Open Assignments for this lesson’s practice lab and acceptance checks.
Common mistakes
Do not benchmark by counting console lines alone. A Profiler callback can report a subtree commit even when a memoized child did little work. Do not add useMemo to every small expression.
Check your understanding
- Predict the output before running the example, then compare it with the result panel.
- Explain which value each expression reads and which action changes it.
- Try the boundary cases listed in the practice task and record what you observe.
- Rebuild one matching assignment without copying, then compare behavior with the original example.
Reference
Official documentation for this topic · Vite setup requirements
