Lesson 4 of 5
Performance and rendering: what makes React apps slow and how to fix it
3 min read
Most React performance problems come from three causes: rendering too much, rendering too often, or doing too much work per render. Always measure first with the React DevTools Profiler ("Highlight updates when components render") before optimising.
React.memo
React.memo(Component) skips re-rendering if the props are shallowly equal to last time.
const Row = React.memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items }) {
const [selected, setSelected] = useState(null);
// Without useCallback, a new function every render breaks Row's memo
const onSelect = useCallback((id) => setSelected(id), []);
return <ul>{items.map((i) => <Row key={i.id} item={i} onSelect={onSelect} />)}</ul>;
}Shallow comparison means objects, arrays and functions created during render are "new" each time. That's why React.memo, useCallback and useMemo usually go together.
Cheaper wins before memo
- Move state down. If only a small part of the page changes (a text input), put that state in a small component so the rest doesn't re-render.
- Lift content up / pass children. A component that changes often can receive the expensive part as
children; children created by the parent don't re-render when the wrapper's state changes. - Avoid creating new objects in props for memoized children.
- Split context so a change in one value doesn't re-render every consumer.
Big lists: virtualization
Rendering 10,000 rows creates 10,000 DOM nodes. Virtualization (react-window, TanStack Virtual) renders only the rows visible in the viewport, plus a small buffer. Mention this for any "large table" or "infinite feed" question, along with pagination or infinite scroll with an IntersectionObserver.
Code splitting and lazy loading
const AdminPanel = React.lazy(() => import("./AdminPanel"));
<Suspense fallback={<Spinner />}>
{isAdmin && <AdminPanel />}
</Suspense>The admin code is downloaded only when needed. Route-based splitting (each page its own bundle) is the biggest, easiest win. Frameworks like Next.js do it automatically.
Concurrent features
useTransitionmarks an update as non-urgent so typing stays responsive while a heavy list re-renders:startTransition(() => setFilter(value)).useDeferredValue(value)gives you a lagging copy of a value for expensive children.
Debouncing and throttling
For search-as-you-type, debounce the request (wait until typing stops). For scroll and resize handlers, throttle (run at most every N ms). Both reduce work and network calls.
Network and assets
Performance is not only rendering:
- Cache server data (TanStack Query, SWR) instead of refetching on every mount.
- Compress and size images, use modern formats (WebP/AVIF) and
loading="lazy". - Keep bundles small: check what a library adds before installing it, import only what you use.
- Server rendering (SSR) or static generation improves first paint and SEO.
A good interview answer structure
When asked "How would you optimise a slow React page?":
- Reproduce and measure (Profiler, Lighthouse, network tab).
- Find the cause: unnecessary re-renders, expensive calculations, big lists, large bundles, slow APIs.
- Apply the matching fix: move state, memoize, virtualize, split code, cache data.
- Measure again to confirm.
Check yourself
- Why does passing an inline arrow function break
React.memo? - What is virtualization and when do you need it?
- What does
useTransitiondo?
Sign up free to save your progress.