Pitfalls: structure & performance
Why memo often does not help – and what does
Most speed problems in React are solved by restructuring, not by memoising. And most “problems” are not problems at all.
Before you optimise anything: the vast majority of React apps are fast enough. And when they are not, the cause is surprisingly rarely the drawing.
Open the ProfilerA measuring tool in the React DevTools that shows which component took how long.→ in the React DevTools and record the interaction. It tells you which component took how long and why it redrew at all. Without measuring you are optimising guesses.
1. memo does nothing when the props are always new
const Child = memo(ItemList);
<Child
items={data.filter(Boolean)} // a new array
style={{ padding: 8 }} // a new object
onClick={() => pick(id)} // a new function
/> const style = { padding: 8 }; // outside the component
const visible = useMemo(() => data.filter(Boolean), [data]);
const handleClick = useCallback(() => pick(id), [id]);
<Child items={visible} style={style} onClick={handleClick} /> memo compares props on the surface. One unstable prop is enough for the comparison to fail every time – and then the optimisation only costs time.
2. The children trick
function Page() {
const [n, setN] = useState(0);
return (
<div>
<button onClick={() => setN(n + 1)}>{n}</button>
<HeavyTree />
</div>
);
} function Page({ children }) {
const [n, setN] = useState(0);
return (
<div>
<button onClick={() => setN(n + 1)}>{n}</button>
{children}
</div>
);
}
// Parent:
<Page><HeavyTree /></Page> children is created in the parent. When Page redraws, that children element is the same object as before – so React skips the subtree entirely, without any memo.
3. State as deep as possible
// ❌ the search text lives in App -> every key press redraws everything
function App() {
const [q, setQ] = useState("");
return (
<>
<Header />
<Search q={q} setQ={setQ} />
<HugeList />
<Footer />
</>
);
}
// ✅ the text lives where it is used
function Search() {
const [q, setQ] = useState("");
return <input value={q} onChange={(e) => setQ(e.target.value)} />;
} 4. Context as a redraw cannon
// One context holding everything -> any change redraws every consumer
<AppContext value={{ user, theme, cart, notifications }}>
// Better: split by how often things change
<UserContext value={user}>
<ThemeContext value={theme}>
<CartContext value={cart}> 5. Large lists
Past a few hundred visible rows no amount of memo helps – at that point the page itself is the problem. The answer is VirtualisationFor very long lists, only actually rendering the rows that are currently visible.→ (@tanstack/react-virtual, react-window): only the visible rows actually exist.
6. What is actually slow
Common real causes – roughly in this order:
1. JavaScript bundles that are too big -> code splitting, dynamic imports
2. Images with no size or modern format -> next/image, modern formats
3. Waterfall requests -> load in parallel, fetch on the server
4. Huge lists in the page -> virtualise
5. Expensive computations -> useMemo, web workers
Rarely the cause:
• "the component draws twice"
• a missing memo wrapper - Change the structure (move state, use
children, split things up). 2. Prioritise drawing (useTransition). 3. Only then memoise. 4. And sooner or later the React Compiler takes that over anyway.
Does it stick?
5 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
HeavyTree counts how often it was drawn. Right now it redraws on every click of the counter. Restructure things so the counter no longer touches the subtree – without using memo.
- □The counter still works
- □After three clicks
HeavyTreeis still at one draw - □No
React.memois used
import { useRef, useState } from "react"; function HeavyTree() { const renders = useRef(0); renders.current += 1; return <p data-testid="heavy">Renders: {renders.current}</p>; } export default function App() { const [n, setN] = useState(0); // TODO: restructure so HeavyTree is untouched by the counter return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <button onClick={() => setN((v) => v + 1)}>Counter: {n}</button> <HeavyTree /> </div> ); }