React DevTools Profiler
Stop Guessing, Start Measuring
Here's a hard truth: most React performance work is just educated guessing. "I think this component re-renders too much." "That list feels slow." The Profiler replaces all that guesswork with hard data. It tells you exactly what rendered, why, and how long it took.
// You think this component is slow. Is it?
function Dashboard() {
const [data, setData] = useState(null);
// ... 20 child components
return (
<div>
<Header />
<Sidebar />
<ChartGrid data={data} />
<ActivityFeed />
</div>
);
}
// The Profiler will tell you:
// - Header took 0.2ms (not a problem)
// - Sidebar took 0.5ms (not a problem)
// - ChartGrid took 180ms (THE problem)
// - ActivityFeed took 1.2ms (not a problem)
Without profiling, you might waste time optimizing Header and Sidebar. With profiling, you go straight to ChartGrid.
The Mental Model
Think of the React Profiler as a medical diagnostic tool. A patient (your app) comes in with symptoms ("it feels slow"). Instead of guessing the illness, you run tests. The flamegraph is like an MRI — it shows the internal structure and where the inflammation (slow renders) is concentrated. The ranked chart is like a blood test — it lists the top offenders by severity. "Why did this render?" is like asking the patient's history — what triggered the symptom.
Without diagnostics, you'd prescribe medicine (React.memo) to every organ just in case. With diagnostics, you treat the specific problem.
Setting Up the Profiler
1. Install React DevTools
React DevTools is available as a browser extension (Chrome, Firefox, Edge) or as a standalone app. After installing, you'll see "Components" and "Profiler" tabs in your browser devtools.
2. Enable "Why did this render?"
In React DevTools settings (gear icon):
- Check "Record why each component rendered while profiling"
- This adds render reason information to the flamegraph
3. Profile a User Interaction
- Open the Profiler tab
- Click the Record button (blue circle)
- Perform the interaction you want to measure (click, type, navigate)
- Click Stop recording
- Analyze the results
The Flamegraph
The flamegraph shows every component that rendered, organized by tree structure:
Commit #1: 145ms
┌──────────────────────────────────────────────┐
│ App (2.1ms) │
├──────────────────────────────────────────────┤
│ Header (0.3ms) │ Main (140ms) │
│ │ │
│ ├─────────────────────────────┤
│ │ SearchBar │ ChartGrid │
│ │ (1.2ms) │ (135ms) │
│ │ │ │
│ │ ├────┬────┬───────┤
│ │ │C1 │C2 │C3 │
│ │ │45ms│45ms│45ms │
└──────────────────────────────────────────────┘
Color coding:
- Gray: Component didn't render (was skipped by memo or bailout)
- Blue-green: Fast render (< 5ms)
- Yellow: Medium render (5-50ms)
- Orange-red: Slow render (> 50ms)
Wide bars mean long render times. Gray bars mean the component was correctly skipped.
The Ranked Chart
This is where you should start every investigation. The ranked chart sorts components by render time, most expensive first:
Ranked Chart — Commit #1
1. ChartGrid 135.2ms
2. Chart (x3) 45.0ms each
3. DataTable 8.5ms
4. SearchBar 1.2ms
5. Header 0.3ms
This immediately tells you where to focus optimization efforts. Don't optimize Header (0.3ms) when ChartGrid takes 135ms.
"Why Did This Render?"
This is the killer feature. When enabled, the Profiler shows the reason each component rendered:
| Reason | Meaning |
|---|---|
| "The parent component rendered" | Parent re-rendered → child follows |
| "Props changed: items, onClick" | Specific props that changed (for memo'd components) |
| "State changed: count" | Component's own state triggered the render |
| "Context changed" | A consumed context value changed |
| "Hooks changed: useState, useMemo" | Hook values changed |
This is the most valuable diagnostic. If a component renders because "the parent rendered" and it produces the same output, that's a candidate for React.memo. If it renders because "props changed: onClick" and onClick is an inline function, you need useCallback.
Interpreting Results
Finding Wasted Renders
Look for components that:
- Render frequently (many commits show them as colored, not gray)
- Are expensive (wide bars in flamegraph)
- Render because of parent, not their own state/props
These are your optimization targets.
Finding Slow Components
Look for:
- Components with render times > 16ms (causing dropped frames)
- Components that account for > 50% of the commit time
- Components with many rendered children (high subtree cost)
Comparing Before/After
Profile before optimization. Save screenshots. Profile after optimization. Compare:
- Did the target component's render time decrease?
- Did the number of renders decrease?
- Did the total commit time decrease?
If not, the optimization didn't help. Remove it to avoid unnecessary complexity.
The Profiler runs in development mode, which is 2-10x slower than production. Absolute times are unreliable. Focus on relative comparisons: "Component A takes 10x longer than Component B" is meaningful. "Component A takes 50ms" is not (it might take 5ms in production).
For production-accurate measurements, use React's <Profiler> component:
function onRender(id, phase, actualDuration) {
// Send to analytics
analytics.track('react_render', { component: id, duration: actualDuration });
}
<Profiler id="ChartGrid" onRender={onRender}>
<ChartGrid data={data} />
</Profiler>The React Profiler Component
For production measurement, use the built-in <Profiler> component:
import { Profiler } from 'react';
function onRender(
id, // "ChartGrid"
phase, // "mount" | "update"
actualDuration, // Time spent rendering the committed update
baseDuration, // Time for the entire subtree without memoization
startTime, // When React started rendering
commitTime // When React committed the update
) {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
function Dashboard() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<ChartGrid />
</Profiler>
);
}
baseDuration is particularly useful: it shows how long the render would take without any memoization. Comparing actualDuration to baseDuration tells you how much your memoization is saving.
Production Scenario: Systematic Performance Investigation
"The app is slow." You've heard it. We've all heard it. Here's how you actually investigate instead of randomly adding React.memo everywhere:
- Reproduce: Identify the specific interaction that feels slow
- Profile: Record the interaction in the Profiler
- Identify: Find the slowest component in the ranked chart
- Diagnose: Check "Why did this render?" — is it necessary?
- Fix: Apply the appropriate optimization
- Verify: Profile again. Compare before/after
Investigation log:
- Interaction: typing in search box
- Commit time: 180ms per keystroke
- Ranked chart: ProductGrid (150ms), ProductCard x200 (0.7ms each)
- Why rendered: "Parent component rendered" for all ProductCards
- Diagnosis: ProductGrid re-renders 200 cards on every keystroke
- Fix: React.memo on ProductCard + useMemo for filtered products
- After: Commit time 15ms (12x improvement)
Common Mistakes
| What developers do | What they should do |
|---|---|
| Optimizing based on development mode timings Development mode is 2-10x slower due to extra checks (StrictMode, prop validation). A 50ms dev render might be 5ms in production | Use relative comparisons in dev mode. Use the Profiler component for production metrics |
| Optimizing components that render fast but often Fast components rendering often is normal React behavior. The cost is usually negligible compared to one slow component | Focus on components that are both slow AND frequent. A 0.1ms component rendering 100 times costs 10ms — likely not the bottleneck |
| Adding React.memo everywhere after seeing gray bars in the flamegraph Gray = good (skipped). Colored = rendered. Optimizing already-gray components does nothing | Gray bars mean the component DID NOT render (already optimized). Focus on colored bars with long durations |
| Never removing optimizations that didn't help Unused memo/useCallback/useMemo add complexity, increase bundle size, and make code harder to maintain — all for zero benefit | Profile before AND after. If the optimization didn't measurably improve performance, remove it |
Challenge
Challenge: Interpret the profiler output
Profiler output for a single commit (user typed 'a' in search):
Flamegraph:
App (0.5ms)
├── SearchBar (0.8ms) — "State changed: query"
├── FilterPanel (0.3ms) — "Parent component rendered"
├── ProductGrid (165ms) — "Props changed: products"
│ ├── ProductCard (0.7ms) × 200 — "Parent component rendered"
│ └── LoadMoreButton (0.1ms) — "Parent component rendered"
└── Footer (0.2ms) — "Parent component rendered"
Total commit time: 167ms
Questions:
1. What is the primary performance bottleneck?
2. Which renders are wasted?
3. What specific optimizations would you apply?
4. What would the commit time be after optimization?
Show Answer
1. Primary bottleneck: ProductGrid at 165ms, which is 98.8% of the total commit time. It renders 200 ProductCard components.
2. Wasted renders:
FilterPanel(0.3ms) — rendered because parent rendered, not because it needs toFooter(0.2ms) — rendered because parent rendered- Most of the 200
ProductCardrenders — they rendered because "Parent component rendered," not because their product data changed. Only products matching the new filter actually need to update LoadMoreButton(0.1ms) — parent rendered
3. Optimizations:
React.memo(ProductCard)— prevents re-rendering cards whoseproductprop hasn't changed. With filtering, most cards show the same product.useMemofor filtered products —const products = useMemo(() => filter(allProducts, query), [allProducts, query])ensures theproductsreference only changes when the filter result actually changes.- State colocation — if
FooterandFilterPaneldon't need search query, consider extracting the search into its own component to prevent their re-renders.
4. Expected commit time after optimization:
- SearchBar: 0.8ms (unchanged — state owner)
- ProductGrid: ~5ms (only re-renders cards whose visibility changed)
- ProductCard: ~0.7ms each, but only 10-20 cards update instead of 200
- Total: ~15-20ms (10x improvement)
Quiz
Key Rules
- 1Profile before optimizing. The Profiler shows exactly which components are slow and why — stop guessing.
- 2Flamegraph shows structure and timing. Ranked chart shows the top offenders. 'Why did this render?' shows the trigger.
- 3Focus on components that are both slow (wide bars) AND render frequently. Fast renders that happen often are rarely the bottleneck.
- 4Gray bars are good — the component was correctly skipped. Optimize colored bars with long durations.
- 5Dev mode times are 2-10x slower than production. Use relative comparisons, not absolute timings. Use the Profiler component for production metrics.
- 6Always profile before AND after optimization. Remove optimizations that don't show measurable improvement.