React Interview Prep
Performance Optimization

Identifying Performance Problems

Finding What Slows Your React App

LinkedIn Hook

Most React developers optimize blindly. They wrap everything in useMemo, slap React.memo on every component, and hope for the best. When an interviewer asks "how would you identify the actual performance bottleneck?" — they have no answer.

Performance optimization without measurement is guesswork. And guesswork in interviews gets you rejected.

The React DevTools Profiler can show you exactly which components re-rendered, how long each render took, and what triggered it. The why-did-you-render library catches unnecessary re-renders in development before they ever reach production. And understanding the common bottlenecks — large unvirtualized lists, cascading re-renders from poorly placed state, heavy computations running on every render — separates senior developers from those still guessing.

Yet most tutorials jump straight to solutions (memoization, virtualization, code splitting) without teaching you how to find the problem first. That is like prescribing medicine without a diagnosis.

In interviews, they will give you a slow React app and ask you to diagnose it. They want to see your process: open the Profiler, record an interaction, read the flame graph, identify the expensive component, explain why it re-rendered, and then propose a targeted fix.

In this lesson, I break down the full diagnostic toolkit: React DevTools Profiler flame graphs and ranked charts, the why-did-you-render library for automatic detection, the browser Performance tab for runtime analysis, and the four most common bottlenecks you will encounter in every React codebase.

If you have ever said "I think the problem is here" instead of "the Profiler shows the problem is here" — this lesson will change your debugging process.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #Performance #WebDevelopment #CodingInterview #100DaysOfCode


Identifying Performance Problems thumbnail


What You'll Learn

  • How to use the React DevTools Profiler to record interactions and read flame graphs
  • How to set up and interpret the why-did-you-render library for automatic re-render detection
  • How to identify the four most common React performance bottlenecks
  • How to measure render performance programmatically with the Profiler component
  • How to use the browser Performance tab to catch runtime issues beyond React

The Concept — Diagnose Before You Prescribe

Analogy: The Doctor's Examination

Imagine you walk into a doctor's office and say "my body feels slow." A bad doctor would immediately prescribe every vitamin, painkiller, and supplement on the shelf. A good doctor would run tests first — blood work, X-rays, heart rate monitoring — to find the specific problem before recommending treatment.

React performance optimization works the same way. Your app "feels slow," and the instinct is to immediately start wrapping components in React.memo, memoizing every value with useMemo, and caching every callback with useCallback. But without measuring first, you are prescribing random medicine.

The React DevTools Profiler is your blood test — it records what happened during an interaction and shows you exactly which components rendered, how long each took, and how many times they rendered. The flame graph is your X-ray — it gives you a visual map of the component tree with hot spots highlighted. The why-did-you-render library is your heart rate monitor — it runs continuously during development and alerts you the moment a component re-renders unnecessarily.

And just like a doctor knows the common conditions — high blood pressure, vitamin deficiency, inflammation — you need to know the common React bottlenecks: large lists rendering thousands of DOM nodes, parent state changes cascading re-renders to dozens of children, heavy computations recalculating on every render, and context providers causing the entire tree to update.

The rule is simple: measure, identify, then optimize. Never the other way around.


React DevTools Profiler

The Profiler is built into React DevTools (browser extension). It records component renders during an interaction and displays the results as flame graphs, ranked charts, and component-level timing data.

Code Example 1: Setting Up the React Profiler API

import { Profiler, useState } from "react";

// The onRender callback receives timing data for every render
// React calls this function after each committed render within the Profiler tree
function onRenderCallback(
  id,           // The "id" prop of the Profiler tree that committed
  phase,        // "mount" (first render) or "update" (re-render)
  actualDuration, // Time spent rendering the committed update (ms)
  baseDuration,   // Estimated time to render the entire subtree without memoization
  startTime,      // When React began rendering this update
  commitTime      // When React committed this update
) {
  // Log performance data — in production, send this to your analytics service
  console.table({
    id,
    phase,
    actualDuration: `${actualDuration.toFixed(2)}ms`,
    baseDuration: `${baseDuration.toFixed(2)}ms`,
    startTime: `${startTime.toFixed(2)}ms`,
    commitTime: `${commitTime.toFixed(2)}ms`,
  });
}

function ExpensiveList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name} — ${item.price}</li>
      ))}
    </ul>
  );
}

function App() {
  const [items, setItems] = useState([
    { id: 1, name: "Laptop", price: 999 },
    { id: 2, name: "Phone", price: 699 },
    { id: 3, name: "Tablet", price: 449 },
  ]);

  const [counter, setCounter] = useState(0);

  return (
    <div>
      {/* Wrap any subtree with Profiler to measure its render performance */}
      <Profiler id="ExpensiveList" onRender={onRenderCallback}>
        <ExpensiveList items={items} />
      </Profiler>

      {/* This button triggers a re-render of App, but does ExpensiveList re-render too? */}
      <button onClick={() => setCounter((c) => c + 1)}>
        Counter: {counter}
      </button>
    </div>
  );
}

// Clicking the counter button logs:
// {
//   id: "ExpensiveList",
//   phase: "update",
//   actualDuration: "0.30ms",
//   baseDuration: "0.45ms",
//   startTime: "1234.56ms",
//   commitTime: "1235.01ms"
// }
// The list re-renders even though 'items' did not change — a waste we can now measure

Key point: The actualDuration tells you how long the render actually took. The baseDuration tells you how long it would take without any memoization. If actualDuration is close to baseDuration, memoization is not helping. If actualDuration is much smaller, your memoization is working.


Detecting Unnecessary Re-Renders with why-did-you-render

The @welldone-software/why-did-you-render library patches React in development mode to log warnings whenever a component re-renders with the same props or state. It catches the silent performance killers that the Profiler alone does not explain.

Code Example 2: Setting Up why-did-you-render

// File: src/wdyr.js — this file MUST be imported before React
import React from "react";

if (process.env.NODE_ENV === "development") {
  const whyDidYouRender = require("@welldone-software/why-did-you-render");

  // Patch React to track unnecessary re-renders
  whyDidYouRender(React, {
    trackAllPureComponents: true, // Track all React.memo and PureComponent
    trackHooks: true,             // Track hook-caused re-renders
    logOnDifferentValues: false,  // Only log when props/state are the same (unnecessary renders)
  });
}

// File: src/index.js — import wdyr BEFORE anything else
import "./wdyr";     // Must be the very first import
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

// File: src/UserCard.jsx — mark specific components for tracking
import { useState, useEffect } from "react";

function UserCard({ user }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// Tell why-did-you-render to track this component
UserCard.whyDidYouRender = true;

function App() {
  const [count, setCount] = useState(0);

  // BUG: Creating a new object on every render
  // Even though the values are the same, the reference changes
  const user = { name: "Alice", email: "alice@example.com" };

  return (
    <div>
      <UserCard user={user} />
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
    </div>
  );
}

// Console output when clicking the button:
// [why-did-you-render] UserCard
// Re-rendered because of props changes:
//   user: { prev: { name: "Alice", email: "alice@example.com" },
//           next: { name: "Alice", email: "alice@example.com" } }
//   -> But values are EQUAL — only the reference changed.
//
// This tells you: move 'user' outside the component or memoize it with useMemo

Key point: The library does not just tell you that a component re-rendered. It tells you why — which prop or state changed, and whether the change was a genuine new value or just a new reference to the same data. This is the difference between a necessary and unnecessary re-render.


Common Performance Bottlenecks

Understanding the four most common bottlenecks lets you know where to look before you even open the Profiler.

Code Example 3: Identifying and Measuring a Cascading Re-Render Problem

import { useState, memo, Profiler } from "react";

// Simulating a component tree where parent state changes cascade to all children

function Header() {
  console.log("Header rendered");
  return <header><h1>My App</h1></header>;
}

function Sidebar({ items }) {
  console.log("Sidebar rendered");
  return (
    <aside>
      {items.map((item) => (
        <div key={item}>{item}</div>
      ))}
    </aside>
  );
}

function Footer() {
  console.log("Footer rendered");
  return <footer><p>Footer content</p></footer>;
}

// This component holds state at the top — every state change re-renders ALL children
function App() {
  const [inputValue, setInputValue] = useState("");
  const [items] = useState(["Home", "About", "Contact"]);

  // Track how many renders happen across the entire tree
  function onRender(id, phase, actualDuration) {
    console.log(`[Profiler] ${id}: ${phase} took ${actualDuration.toFixed(2)}ms`);
  }

  return (
    <Profiler id="App" onRender={onRender}>
      <div>
        {/* Typing in this input triggers a re-render of App */}
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Type something..."
        />

        {/* All three of these re-render on every keystroke
            even though NONE of them depend on inputValue */}
        <Header />
        <Sidebar items={items} />
        <Footer />
      </div>
    </Profiler>
  );
}

// Typing "hello" (5 keystrokes) produces:
// Header rendered    (x5)
// Sidebar rendered   (x5)
// Footer rendered    (x5)
// [Profiler] App: update took 2.15ms (x5)
//
// Problem: 15 unnecessary renders for a simple text input
// Solution: Move inputValue state into its own component, or memo the children

Code Example 4: Heavy Computation Running on Every Render

import { useState, useMemo } from "react";

// A computationally expensive function — simulates processing a large dataset
function findPrimeNumbers(limit) {
  console.log(`Computing primes up to ${limit}...`);
  const primes = [];
  for (let num = 2; num <= limit; num++) {
    let isPrime = true;
    for (let i = 2; i <= Math.sqrt(num); i++) {
      if (num % i === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) primes.push(num);
  }
  return primes;
}

function PrimeCalculator() {
  const [limit, setLimit] = useState(50000);
  const [theme, setTheme] = useState("light");

  // BAD: This runs on EVERY render, including when only 'theme' changes
  // const primes = findPrimeNumbers(limit);

  // GOOD: useMemo recalculates only when 'limit' changes
  const primes = useMemo(() => findPrimeNumbers(limit), [limit]);

  // Measure how you would identify this problem:
  // 1. Open React DevTools Profiler
  // 2. Click "Record"
  // 3. Toggle the theme button
  // 4. Stop recording
  // 5. Look at the flame graph — PrimeCalculator shows a long bar
  // 6. The render duration is high even though only 'theme' changed
  // 7. This tells you: an expensive computation is running unnecessarily

  return (
    <div style={{ background: theme === "dark" ? "#222" : "#fff" }}>
      <h2>Prime Numbers up to {limit}</h2>
      <p>Found {primes.length} primes</p>

      <button onClick={() => setLimit((l) => l + 10000)}>
        Increase Limit
      </button>

      {/* Toggling theme should be instant — but without useMemo, it recalculates primes */}
      <button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
        Toggle Theme: {theme}
      </button>
    </div>
  );
}

// Without useMemo — clicking "Toggle Theme":
// Console: "Computing primes up to 50000..."  (takes ~200ms)
// The theme toggle feels sluggish

// With useMemo — clicking "Toggle Theme":
// Console: (nothing — cached result is reused)
// The theme toggle is instant

Key point: The Profiler would show PrimeCalculator taking 200ms on a theme toggle. That mismatch between "what changed" (theme) and "how long it took" (200ms) is the diagnostic signal. The Profiler does not tell you to use useMemo — it tells you something expensive is happening when it should not be, and you investigate from there.


Identifying Performance Problems visual 1


Identifying Performance Problems visual 2


Common Mistakes

Mistake 1: Optimizing without measuring first

// BAD: Wrapping everything in React.memo "just in case"
const Header = memo(function Header() {
  return <h1>My App</h1>;
});

const Footer = memo(function Footer() {
  return <footer>Footer</footer>;
});

const Sidebar = memo(function Sidebar({ items }) {
  return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
});

// Problem: You added complexity to every component without knowing
// if any of them were actually causing performance issues.
// React.memo has a cost — it must shallow-compare props on every render.
// If the component is cheap to render, memo can actually make it SLOWER.

// GOOD: Profile first, find the bottleneck, memo only what is expensive
// Open Profiler -> Record -> Interact -> Read flame graph -> Identify slow component
// Then apply memo/useMemo/useCallback only to that specific component

Mistake 2: Using the production build for profiling

# BAD: Testing performance on the development build
npm start
# Development mode includes extra checks, warnings, and unminified code
# Your app will be 2-10x slower than production — measurements are misleading

# GOOD: Use the profiling build for accurate measurements
npx react-scripts build --profile
# Or in Vite:
npx vite build --mode development
# Then serve the build locally:
npx serve -s build

# The --profile flag keeps component names in the production build
# so the Profiler can still show meaningful names instead of minified ones

Mistake 3: Ignoring the browser Performance tab

Many developers only use React DevTools and miss problems that happen
outside React's rendering cycle:

- Long JavaScript tasks blocking the main thread (layout thrashing, forced reflow)
- Expensive CSS selectors or animations causing layout recalculation
- Network waterfalls where components fetch data sequentially
- Memory leaks from event listeners or intervals not cleaned up in useEffect

The browser Performance tab (Chrome DevTools -> Performance) shows EVERYTHING:
- JavaScript execution time (scripting)
- Layout and paint operations (rendering)
- Network requests and their timing
- Main thread blocking and frame drops

Use React DevTools Profiler for React-specific diagnosis.
Use the browser Performance tab for the full picture.

Interview Questions

Q: How would you diagnose a slow React application? Walk me through your process.

First, reproduce the slowness with a specific interaction — clicking a button, typing in an input, scrolling a list. Then open React DevTools Profiler, click Record, perform the interaction, and stop recording. Read the flame graph: the widest bars are the slowest components. Check the "Why did this render?" column to see what triggered each render. If a component rendered but its props did not change, that is an unnecessary re-render. Then cross-reference with the browser Performance tab to check for non-React issues like layout thrashing or long tasks. Only after identifying the specific bottleneck do I choose an optimization strategy.

Q: What is the difference between actualDuration and baseDuration in the React Profiler?

actualDuration is the time React actually spent rendering the component and its descendants in this commit. baseDuration is the estimated time to render the entire subtree without any memoization (memo, useMemo, useCallback). If actualDuration is much smaller than baseDuration, your memoization is working well — React skipped re-rendering memoized subtrees. If they are close, memoization is either not applied or not effective for that component tree.

Q: What are the most common causes of unnecessary re-renders in React?

Four main causes: (1) Creating new object or array references in the render body that get passed as props — even if the values are identical, the reference changes, so the child re-renders. (2) Inline function definitions passed as props — each render creates a new function reference. (3) State stored too high in the component tree — updating one piece of state re-renders all siblings that do not depend on it. (4) Context value changing too frequently — every consumer re-renders when the provider value changes, even if the consumer only uses a portion of the context that did not change.

Q: When should you NOT use React.memo?

Do not use React.memo when: the component is cheap to render and the shallow comparison cost outweighs the render cost; when the component almost always receives new props (memo adds overhead without preventing renders); when the component receives children as props (children are new JSX elements on every render, defeating memo); or when you have not measured and confirmed that the component is actually causing a performance problem. Premature memoization adds code complexity for no benefit.

Q: How does the why-did-you-render library help during development?

It patches React to automatically detect and log unnecessary re-renders — cases where a component re-rendered but received the same prop values (different references, same content). It shows exactly which prop changed, what the previous and next values were, and whether the values were deeply equal. This catches the most common performance bug in React: passing new object/array/function references that look the same but trigger re-renders because referential equality fails. It runs only in development and has zero production impact.


Quick Reference — Cheat Sheet

IDENTIFYING PERFORMANCE PROBLEMS
==================================

Diagnostic Tools:
  React DevTools Profiler:
    - Install: React DevTools browser extension
    - Open: DevTools -> Profiler tab
    - Record: Click record -> interact -> stop
    - Flame graph: Width = render duration, color = cost
    - Ranked chart: Components sorted by render time
    - "Why did this render?": Enable in Profiler settings

  React Profiler API:
    <Profiler id="name" onRender={callback}>
      <Component />
    </Profiler>
    Callback params: (id, phase, actualDuration, baseDuration, startTime, commitTime)

  why-did-you-render:
    Install:  npm install @welldone-software/why-did-you-render --save-dev
    Setup:    Import wdyr.js BEFORE React in entry file
    Track:    ComponentName.whyDidYouRender = true
    Or:       trackAllPureComponents: true (global)

  Browser Performance Tab:
    Open:     DevTools -> Performance -> Record -> Interact -> Stop
    Shows:    JS execution, layout, paint, network, memory

4 Common Bottlenecks:
  +------------------------+---------------------------+-------------------+
  | Bottleneck             | Symptom                   | Fix               |
  +------------------------+---------------------------+-------------------+
  | Large lists            | Thousands of DOM nodes    | Virtualization    |
  | Cascading re-renders   | Parent update -> all kids | memo / split state|
  | Heavy computations     | Slow renders, same data   | useMemo           |
  | Context over-broadcast | Provider change -> all    | Split contexts    |
  +------------------------+---------------------------+-------------------+

Profiler Reading Guide:
  actualDuration << baseDuration  -> Memoization is working
  actualDuration ~= baseDuration  -> No memoization benefit
  High actualDuration on update   -> Component is expensive to re-render
  Many renders for one interaction -> Cascading re-renders

Measurement Rules:
  1. Always profile the PRODUCTION build (with --profile flag)
  2. Measure a specific interaction, not "the whole app"
  3. Record multiple runs — single measurements have noise
  4. Compare before/after to verify your optimization helped
  5. Never optimize what you have not measured

Previous: Lesson 7.3 — Protected Routes & Route Guards -> Next: Lesson 8.2 — List Virtualization ->


This is Lesson 8.1 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page