Performance Profiling and Optimization

Course 3 · Ch 6
Performance Profiling and Optimization
The tools for actually measuring before reaching for Chapter 7's useMemo/useCallback/memo

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.

"Highlight updates when components render"
A settings toggle in the Profiler tab outlines every component on the actual page with a colored flash the instant it re-renders — no recording needed at all. It's the fastest way to spot an unexpectedly large swath of the UI flashing on every keystroke or click, exactly the kind of "the whole page re-renders when only one small thing changed" problem Chapter 7's tools exist to fix.

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 with useMemo/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's value object (also Intermediate Ch 7).

Long Lists — Virtualization

// npm install react-window import { FixedSizeList } from "react-window"; function BigList({ items }) { return ( <FixedSizeList height={400} itemCount={items.length} itemSize={35}> {({ index, style }) => <div style={style}>{items[index]}</div>} </FixedSizeList> ); }

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

# development mode (npm run dev) is NOT representative of real performance npm run build npm run preview # serves the actual production build locally

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.

Still don't optimize without a number
Every tool in this chapter exists to produce an actual measurement — a flame graph, a render-duration number, a bundle size in kilobytes — before any code changes. "This component feels like it might be slow" is not a measurement; a Profiler recording showing it taking 40ms across 200 renders in a 10-second interaction is. Chapter 7's warning bears repeating here with the tools finally in hand to back it up.

Coding Challenges

Challenge 1

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 solution
Challenge 2

Render 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 solution
Challenge 3

Run 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 solution

Chapter 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