Skip to content

React DevTools Profiler

advanced10 min read

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

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

  1. Open the Profiler tab
  2. Click the Record button (blue circle)
  3. Perform the interaction you want to measure (click, type, navigate)
  4. Click Stop recording
  5. 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.

Execution Trace
Click Record
Profiler starts capturing
React instruments all component renders
Interact
Type in search box
Each keystroke triggers a commit (render + commit cycle)
Stop
Profiler shows results
One bar per commit. Click each to see details
Select commit
Flamegraph appears
Shows component tree with render times
Click component
Detail panel opens
Shows props, state, hooks, and why it rendered

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:

ReasonMeaning
"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:

  1. Render frequently (many commits show them as colored, not gray)
  2. Are expensive (wide bars in flamegraph)
  3. Render because of parent, not their own state/props

These are your optimization targets.

Finding Slow Components

Look for:

  1. Components with render times > 16ms (causing dropped frames)
  2. Components that account for > 50% of the commit time
  3. 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.

Common Trap

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:

  1. Reproduce: Identify the specific interaction that feels slow
  2. Profile: Record the interaction in the Profiler
  3. Identify: Find the slowest component in the ranked chart
  4. Diagnose: Check "Why did this render?" — is it necessary?
  5. Fix: Apply the appropriate optimization
  6. 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 doWhat 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 to
  • Footer (0.2ms) — rendered because parent rendered
  • Most of the 200 ProductCard renders — 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:

  1. React.memo(ProductCard) — prevents re-rendering cards whose product prop hasn't changed. With filtering, most cards show the same product.
  2. useMemo for filtered productsconst products = useMemo(() => filter(allProducts, query), [allProducts, query]) ensures the products reference only changes when the filter result actually changes.
  3. State colocation — if Footer and FilterPanel don'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

Quiz
What does a gray bar in the React DevTools Profiler flamegraph mean?

Key Rules

Key Rules
  1. 1Profile before optimizing. The Profiler shows exactly which components are slow and why — stop guessing.
  2. 2Flamegraph shows structure and timing. Ranked chart shows the top offenders. 'Why did this render?' shows the trigger.
  3. 3Focus on components that are both slow (wide bars) AND render frequently. Fast renders that happen often are rarely the bottleneck.
  4. 4Gray bars are good — the component was correctly skipped. Optimize colored bars with long durations.
  5. 5Dev mode times are 2-10x slower than production. Use relative comparisons, not absolute timings. Use the Profiler component for production metrics.
  6. 6Always profile before AND after optimization. Remove optimizations that don't show measurable improvement.