Performance Profiling and Optimization
Intermediate Chapter 7 introduced memo, useMemo, and useCallback with a clear warning attached: measure first, don't optimize by guessing. This chapter is that measurement — the React DevTools Profiler, recognizing the common causes of unnecessary re-rendering on sight, and the handful of other tools (virtualization, production builds, bundle analysis) that matter once an app is large enough for performance to be a real, measured concern.
The React DevTools Profiler
The React DevTools browser extension adds a "Profiler" tab alongside the regular Elements/Components inspector. Recording a session — clicking the record button, performing some interaction (a click, typing, a re-render), then stopping the recording — produces a flame graph: one bar per component that rendered during that window, sized by render duration, colored by relative cost. Clicking any individual render shows exactly which props or state changed for that component, and a tooltip explaining why it rendered (a parent re-rendered, props changed, hooks changed) — turning "this feels slow" into a specific, named component and a specific, named cause.
Recognizing the Common Causes
Three patterns explain the overwhelming majority of unnecessary re-renders, all already covered with their fixes in earlier chapters — this chapter's job is recognizing them in a profiler's output, not re-teaching the fixes themselves:
- Parent cascade — every child re-renders by default whenever its parent does, regardless of whether that child's own props changed. Fixed with
memo(Intermediate Ch 7). - Unstable references — a new object/array/function created every render defeats
memo's comparison even when the actual content is identical. Fixed withuseMemo/useCallback(Intermediate Ch 7). - Context value changes — every consumer of a context re-renders on any change to its
value, including changes from unrelated state in the same provider. Fixed by memoizing the context'svalueobject (also Intermediate Ch 7).
Long Lists — Virtualization
memo/useMemo help when re-renders happen too often; they don't help when a single render itself is expensive simply because it's creating thousands of DOM nodes at once, the way .map() over a 5,000-item array (Fundamentals Chapter 6) would. Virtualization (also called "windowing") renders only the rows currently visible in the scrollable viewport, recycling the same handful of DOM nodes as the user scrolls — react-window is the standard library for this, used here as a render-props-style component (Chapter 5) passing each visible row's index and positioning style.
Always Profile a Production Build
A Vite dev server runs extra checks, warnings, and unoptimized code specifically to make development easier (better error messages, instant updates) — at the direct cost of real performance accuracy. Profiling in dev mode regularly shows render times several times slower than the same code in a real production build; always build and profile the production output before drawing any real conclusion about whether something is actually slow for an end user.
Bundle Size — Finding What's Actually Large
Before deciding what to lazy-load (Chapter 3), it's worth knowing what's actually contributing to bundle size in the first place — a bundle visualizer (e.g. vite-bundle-visualizer or the official rollup-plugin-visualizer) generates a treemap of every dependency's contribution to the final bundle, often revealing one unexpectedly large library is responsible for most of the weight, which is exactly the kind of thing worth wrapping in lazy() rather than guessing at random.
Coding Challenges
Build a small app with an unrelated counter and an expensive child list (deliberately not yet memoized). Use the React DevTools Profiler (or the "highlight updates" toggle) to confirm the list re-renders when the counter changes, then apply memo/useMemo from Intermediate Chapter 7 and confirm via the same tool that it's fixed.
📄 View solutionRender a list of 2,000+ items with a plain .map() and observe scroll/render performance. Then rebuild the same list using react-window's FixedSizeList and compare.
📄 View solutionRun npm run build followed by npm run preview on any project from this course, and profile a typical interaction in both the dev server and the production preview, noting the difference in reported render times.
📄 View solutionChapter 6 Quick Reference
- React DevTools Profiler — records renders into a flame graph, with a "why did this render" explanation per component
- "Highlight updates" — visually flashes re-rendering components on the live page, no recording needed
- Three common causes: parent cascade, unstable references, context value changes — all fixed in Intermediate Ch 7
- Virtualization (react-window) — renders only visible rows of a very long list
- Always profile a production build (
npm run build && npm run preview) — dev mode numbers are misleading - A bundle visualizer shows what's actually large, informing what's worth code-splitting (Ch 3)
- Next chapter: intro to Next.js / SSR — wrapping up the course with the framework first previewed in Project 7