React DevTools Profiler for Wasted Renders
Stop Guessing, Start Measuring
The biggest performance mistake in React is optimizing based on intuition instead of data. Engineers scatter React.memo, useMemo, and useCallback everywhere "just in case" — adding complexity without knowing if there is a problem. The React DevTools Profiler tells you exactly which components re-render, how long each render takes, and why it happened.
No guessing. No premature optimization. Just data.
The React DevTools Profiler is like a fitness tracker for your component tree. It records every render cycle — which components ran, how long each one took, and what triggered it. Without it, optimizing React performance is like trying to lose weight without a scale: you are just guessing. With it, you see exactly which components are the most expensive, which renders are unnecessary, and where memoization would actually make a difference.
Setting Up the Profiler
- Install the React Developer Tools browser extension
- Open DevTools → Profiler tab (next to Components)
- Click the Settings gear → check "Record why each component rendered while profiling"
- Click the blue Record circle to start profiling
- Interact with your app
- Click Stop
Production builds strip profiling data by default. To profile a production build, use react-dom/profiling instead of react-dom and set the --profile flag in your build. Next.js supports this via next build --profile.
Flamegraph View
The flamegraph shows every component that rendered during a commit. Width represents render duration. Color represents how long the render took relative to other components:
- Yellow/orange — slower renders (potential optimization targets)
- Blue/teal — faster renders
- Grey — components that did NOT render in this commit
Grey is the most important color. When a parent re-renders, you want its children to be grey — it means they were skipped (either by React.memo, bailout from same state, or because they are Server Components).
Ranked View
The Ranked view sorts components by render duration, with the slowest at the top. This is your hit list for optimization.
If a component takes 50ms to render and it renders 10 times during an interaction, that is 500ms of main thread work — a long task that degrades INP. The Ranked view surfaces these expensive components immediately.
What to Look For
- Components with long render times — are they doing expensive computation during render? Should that computation be memoized or moved to a Web Worker?
- Components that render frequently — are they re-rendering on every keystroke? Every mouse move? Every scroll event?
- Components that render but produce the same output — the "why did this render?" column shows the reason. If it says "props changed" but you know the output is the same, memoization would help.
Why Did This Render?
With "Record why each component rendered" enabled, clicking any component in the flamegraph reveals why it rendered:
| Reason | What It Means | Action |
|---|---|---|
| The parent component rendered | Default React behavior — parent re-render triggers children | Consider React.memo if this component's output depends only on its props |
| Props changed | At least one prop received a new reference | Check if the new reference is a new object/function created every render |
| State changed | This component's own useState or useReducer changed | Expected — verify the state change is necessary |
| Hooks changed | A custom hook returned a new value | Check if the hook is creating new objects or functions on every call |
| Context changed | A context value this component consumes changed | Consider splitting the context or memoizing the context value |
Component Highlight on Re-render
The Components tab (not Profiler) has a setting: "Highlight updates when components render". When enabled, components flash with a colored border when they render:
- Blue border — infrequent renders (normal)
- Yellow border — moderate render frequency
- Red border — frequent renders (potential problem)
This is great for catching cascading re-renders in real time. Type in an input, and watch which components flash. If the entire page flashes red on every keystroke, you have a problem.
The Memoization Decision Framework
Here is the data-driven framework for deciding when to memoize. Every decision starts with the Profiler, never with intuition:
When React.memo Helps
Use React.memo when the Profiler shows a component:
- Re-renders frequently due to parent re-renders
- Its own render is expensive (measurably slow)
- Its props are actually the same between renders (or can be made the same with
useMemo/useCallbackin the parent)
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return items.map(item => (
<ExpensiveItem key={item.id} item={item} onSelect={onSelect} />
));
});
When useMemo Helps
Use useMemo when the Profiler shows expensive computation during render:
function Dashboard({ transactions }) {
const summary = useMemo(
() => computeExpensiveSummary(transactions),
[transactions]
);
return <SummaryView data={summary} />;
}
Only memoize if computeExpensiveSummary is measurably slow. If it takes 0.5ms, the memoization overhead (dependency comparison, cache management) is not worth it.
When useCallback Helps
Use useCallback when passing a callback to a memoized child:
function Parent({ items }) {
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
return <MemoizedList items={items} onSelect={handleSelect} />;
}
Without useCallback, handleSelect is a new function every render, defeating MemoizedList's React.memo. But useCallback alone does nothing — it only helps when the receiving component is memoized.
useCallback without React.memo on the child is pure overhead. It creates a memoized function reference, but if the child does not check props (no React.memo), it re-renders anyway. The two must be used together. The Profiler will show you whether the child is being skipped — if it is not grey after adding useCallback, the React.memo wrapper is missing.
React Compiler and the Future of Memoization
React Compiler (formerly React Forget) aims to auto-memoize components and hooks at compile time, eliminating the need for manual React.memo, useMemo, and useCallback. When enabled, the compiler analyzes your component's dependencies and inserts memoization automatically.
However, the Profiler remains essential even with React Compiler:
- The compiler's memoization may not cover all cases
- Complex data flows might still cause unnecessary renders
- Third-party libraries may not be optimized
- The Profiler validates that optimizations are actually working
Profiling Server Components
Server Components do not appear in the React DevTools Profiler because they do not run on the client. If a Server Component's child is a Client Component that re-renders excessively, you will see the Client Component in the Profiler but not the Server Component parent. Server Component performance is measured with server-side tools (Node.js profiler, server logs) and the Network panel (time to first byte for RSC payloads). Use the Performance panel's flame chart to see hydration costs of Server Component output.
- 1Always enable 'Record why each component rendered' in Profiler settings before recording
- 2Use the Ranked view to find the slowest components — optimize the top of the list first
- 3Grey components in the flamegraph are skipped renders — this is the goal for stable components
- 4React.memo only helps when the component's render is measurably expensive AND it re-renders unnecessarily
- 5useCallback is pointless without React.memo on the receiving child — they must be used together
- 6Profile first, memoize second — never add useMemo or useCallback without profiler evidence
| What developers do | What they should do |
|---|---|
| Adding React.memo to every component as a default React.memo adds overhead (prop comparison cost). For cheap components, the comparison costs more than the re-render | Only memoize components the Profiler shows are slow AND re-render unnecessarily |
| Using useMemo for all derived values useMemo has overhead (dependency check, cache). For cheap computations like filtering a 10-item array, just compute it directly | Only memoize computations the Profiler shows take more than 1-2ms |
| Using useCallback without React.memo on the child Without React.memo, the child re-renders regardless of whether its props changed | useCallback stabilizes references — but the child must use React.memo to benefit from stable references |
| Profiling only in development mode React development mode runs additional validations (double-invoking effects, prop type checking) that slow renders by 2-10x | Profile production builds — dev mode has extra checks that skew timing |