Steve Kinney

Measuring Performance with Real Tools

Performance optimization without measurement is just wishful thinking with extra steps. You might feel like that component refactor made things faster, but feelings don’t ship reliable software. Whether you’re tracking down sluggish renders or proving to stakeholders that your optimization sprint was worth it, you need real data from real tools.

React performance debugging used to be a mix of console logs, timing code, and educated guessing. Not anymore. Between React DevTools Profiler, Chrome’s Performance tab, and modern flamegraph tooling, you can pinpoint exactly where your app is spending time—and more importantly, where it’s wasting it.

The React DevTools Profiler: Your First Stop

The React Developer Tools Profiler is purpose-built for React apps and should be your go-to tool for understanding component behavior. It shows you which components rendered, why they rendered, and how long they took.

Getting Started with the Profiler

First, install the React Developer Tools browser extension. Once installed, you’ll see a “Profiler” tab in your browser’s dev tools when viewing a React app.

Here’s how to capture a performance profile:

// Sample component that might have performance issues
const ExpensiveList = ({ items, filter }: { items: Item[]; filter: string }) => {
  // ❌ This filtering happens on every render
  const filteredItems = items.filter((item) =>
    item.name.toLowerCase().includes(filter.toLowerCase()),
  );

  const expensiveCalculation = (item: Item) => {
    // Simulate expensive work
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += item.value * Math.random();
    }
    return result;
  };

  return (
    <div>
      {filteredItems.map((item) => (
        <div key={item.id}>
          <span>{item.name}</span>
          <span>{expensiveCalculation(item)}</span>
        </div>
      ))}
    </div>
  );
};

To profile this component:

  1. Open React DevTools and click the “Profiler” tab
  2. Click the record button (the circle)
  3. Interact with your app (type in a filter, scroll, click buttons)
  4. Click stop recording

Reading Profiler Results

The Profiler shows you a flamegraph of your component tree. Each bar represents a component, and the width shows how long it took to render. Here’s what to look for:

  • Wide bars: Components taking a long time to render
  • Tall stacks: Deep component hierarchies that might benefit from optimization
  • Frequent re-renders: Components updating more than necessary
Click on individual components in the flamegraph to see why they rendered. Common causes include props changes, state updates, and parent re-renders.

The Profiler also shows you the “Ranked” view, which lists components by render duration. This is gold for finding your worst offenders.

Understanding Why Components Rendered

For each component, the Profiler tells you exactly why it rendered:

  • Hook changed: A useState or useReducer hook updated
  • Props changed: Parent passed new props
  • Parent rendered: Component rendered because its parent did
  • State changed: Component’s own state was updated
// Example: Debugging unnecessary re-renders
const ProfileButton = ({ user, onEdit }: { user: User; onEdit: () => void }) => {
  console.log('ProfileButton rendering...', { user, onEdit });

  return <button onClick={onEdit}>Edit {user.name}</button>;
};

// Parent component
const UserProfile = () => {
  const [user, setUser] = useState(/* ... */);

  // ❌ This creates a new function on every render
  const handleEdit = () => {
    // edit logic
  };

  return <ProfileButton user={user} onEdit={handleEdit} />;
};

In the Profiler, you’d see ProfileButton rendering with “Props changed” as the reason—even though user didn’t actually change. The culprit? That handleEdit function getting recreated every render.

Chrome Performance Tab: The Deep Dive

When the React DevTools Profiler shows you what’s slow but you need to understand why, Chrome’s Performance tab gives you the full picture. It shows JavaScript execution, rendering, painting, and everything else happening in the browser.

Recording a Performance Profile

  1. Open Chrome DevTools (F12 or Cmd+Option+I)
  2. Go to the “Performance” tab
  3. Click the record button
  4. Interact with your app for 3-5 seconds
  5. Click stop
Performance profiles can be huge. Keep recordings short (3-10 seconds) and focus on specific interactions.

Reading the Timeline

The Performance tab shows several tracks:

  • Main thread: JavaScript execution, including React renders
  • Compositor: Layer compositing and animations
  • GPU: GPU-accelerated operations
  • Network: Resource loading

Look for:

  • Long tasks (yellow/red blocks): JavaScript blocking the main thread
  • Layout thrashing: Repeated layout calculations
  • Excessive garbage collection: Memory pressure

Finding React Work in the Timeline

React work appears in the Main thread timeline. Look for:

  • Function calls like commitRoot, performWorkUntilDeadline
  • Your component names in the call stack
  • Time spent in ReactDOMRoot.render
// This component will show up clearly in Chrome's timeline
const SlowComponent = ({ data }: { data: number[] }) => {
  const [sortOrder, setSortOrder] = useState('asc');

  // ❌ Expensive sort operation runs on every render
  const sortedData = data.sort((a, b) => (sortOrder === 'asc' ? a - b : b - a));

  return (
    <div>
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        Toggle sort
      </button>
      {sortedData.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
};

In the Performance timeline, you’d see a long block of JavaScript execution when the sort button is clicked, with SlowComponent prominently featured in the call stack.

User Timing API: Custom Performance Markers

Sometimes you need to measure specific operations that don’t align with React’s render cycles. The User Timing API lets you add custom markers to the timeline.

const DataProcessor = ({ rawData }: { rawData: RawData[] }) => {
  const [processedData, setProcessedData] = useState<ProcessedData[]>([]);

  useEffect(() => {
    const processData = async () => {
      // Mark the start of data processing
      performance.mark('data-processing-start');

      const processed = await Promise.all(
        rawData.map(async (item) => {
          performance.mark(`process-item-${item.id}-start`);

          // Simulate async processing
          const result = await processItem(item);

          performance.mark(`process-item-${item.id}-end`);
          performance.measure(
            `process-item-${item.id}`,
            `process-item-${item.id}-start`,
            `process-item-${item.id}-end`,
          );

          return result;
        }),
      );

      performance.mark('data-processing-end');
      performance.measure('data-processing', 'data-processing-start', 'data-processing-end');

      setProcessedData(processed);
    };

    processData();
  }, [rawData]);

  return (
    <div>
      {processedData.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
};

These custom marks and measures appear in both Chrome’s Performance tab and React DevTools, giving you precise timing for your own operations.

Real World Performance Debugging

Let’s walk through debugging a real performance problem. Imagine you have a dashboard with multiple widgets that’s feeling sluggish.

Step 1: Identify the Problem Area

Start with the React DevTools Profiler:

// Dashboard component that might be problematic
const Dashboard = () => {
  const [widgets, setWidgets] = useState<Widget[]>([]);
  const [selectedWidget, setSelectedWidget] = useState<string | null>(null);

  return (
    <div className="dashboard">
      <WidgetList widgets={widgets} selectedWidget={selectedWidget} onSelect={setSelectedWidget} />
      <WidgetDetails widget={widgets.find((w) => w.id === selectedWidget)} />
      <WidgetChart data={/* expensive calculation */} />
    </div>
  );
};

Record a profile while selecting different widgets. You might see WidgetChart taking 500ms to render each time—even when the selected widget doesn’t affect the chart data.

Step 2: Understand Why It’s Rendering

Look at the “Why did this render?” information in the Profiler. You might discover:

  • WidgetChart renders because its parent Dashboard renders
  • Dashboard renders because selectedWidget state changes
  • WidgetChart doesn’t actually use selectedWidget data

Step 3: Use Chrome Performance for Details

Switch to Chrome’s Performance tab to see what WidgetChart is actually doing during those 500ms:

  • Is it JavaScript computation?
  • Layout and paint work?
  • Network requests?

You might find it’s recalculating chart data that hasn’t changed.

Step 4: Optimize and Verify

Apply optimizations like React.memo, useMemo, or useCallback:

// ✅ Optimized version
const WidgetChart = React.memo(({ data }: { data: ChartData[] }) => {
  // Chart rendering logic
  return <div>{/* chart implementation */}</div>;
});

const Dashboard = () => {
  const [widgets, setWidgets] = useState<Widget[]>([]);
  const [selectedWidget, setSelectedWidget] = useState<string | null>(null);

  // ✅ Memoize expensive chart data calculation
  const chartData = useMemo(() => calculateChartData(widgets), [widgets]);

  // ✅ Memoize callback to prevent WidgetList re-renders
  const handleSelect = useCallback((id: string) => {
    setSelectedWidget(id);
  }, []);

  return (
    <div className="dashboard">
      <WidgetList widgets={widgets} selectedWidget={selectedWidget} onSelect={handleSelect} />
      <WidgetDetails widget={widgets.find((w) => w.id === selectedWidget)} />
      <WidgetChart data={chartData} />
    </div>
  );
};

Step 5: Measure Again

Record another profile to confirm your optimizations worked. You should see:

  • WidgetChart no longer renders when selecting widgets
  • Overall interaction time decreased
  • Flamegraph shows less work being done

Advanced Profiling Techniques

Simulating Slower Devices

Chrome lets you throttle CPU performance to simulate slower devices:

  1. Open the Performance tab
  2. Click the gear icon
  3. Set CPU to “4x slowdown” or “6x slowdown”

This reveals performance problems that might not be obvious on your development machine.

Profiling in Production

React DevTools Profiler works in production builds, but you need to enable it explicitly:

// Only enable profiling in development or staging
const shouldProfile =
  process.env.NODE_ENV !== 'production' || process.env.REACT_APP_ENABLE_PROFILING === 'true';

if (shouldProfile) {
  import('react-dom/profiling').then(({ unstable_trace }) => {
    unstable_trace('App render', performance.now(), () => {
      // Your app rendering logic
    });
  });
}
Profiling adds overhead. Don’t leave it enabled in production unless you’re actively debugging.

Memory Profiling

Performance isn’t just about render speed—memory leaks can make your app progressively slower. Use Chrome’s Memory tab to:

  • Take heap snapshots before and after interactions
  • Compare snapshots to find memory leaks
  • Profile allocations to see what’s creating objects
// Common memory leak pattern
const LeakyComponent = () => {
  useEffect(() => {
    const interval = setInterval(() => {
      // This interval keeps running even after component unmounts
      console.log('Still running...');
    }, 1000);

    // ❌ Missing cleanup
    // return () => clearInterval(interval);
  }, []);

  return <div>Component content</div>;
};

Performance Testing in CI/CD

Don’t wait for users to report performance regressions. Integrate performance testing into your build process:

// Performance test using Lighthouse CI
const performanceTest = async () => {
  const lighthouse = await import('lighthouse');
  const puppeteer = await import('puppeteer');

  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  const runnerResult = await lighthouse('http://localhost:3000', {
    port: 9222,
    output: 'json',
    onlyCategories: ['performance'],
  });

  const score = runnerResult.score;

  if (score < 90) {
    throw new Error(`Performance score too low: ${score}`);
  }

  await browser.close();
};

Key Takeaways

Performance optimization is an iterative process: measure, optimize, verify, repeat. Here’s your toolkit:

  1. React DevTools Profiler: Start here for component-level insights
  2. Chrome Performance tab: Deep dive into browser behavior
  3. User Timing API: Add custom measurements for specific operations
  4. Memory profiling: Don’t forget about memory leaks
  5. Automated testing: Prevent regressions with CI/CD integration

Remember: premature optimization might be the root of all evil, but no optimization is the root of all user frustration. Use these tools to find the real bottlenecks, fix them with confidence, and ship faster experiences that users will actually notice.

The next time someone asks “is it faster now?”, you’ll have data instead of hopes and dreams.

Last modified on .