Tools12 min read

Finding Jank in React Apps with Chrome DevTools Performance Tab

A practical walkthrough of the Performance tab for diagnosing frame drops and long tasks in React apps, with a real debugging story.

Chrome DevToolsPerformanceReactJankFlame Chart

The Chrome DevTools Performance tab is one of those tools everyone knows about but few use effectively. Most engineers open it, hit record, see a bunch of colored bars, and close it. I did that for years.

But last quarter, a React app I owned started dropping frames during a routine list scroll. The UX team complained, users started tweeting, and my manager asked me to fix it. I spent a week diving deep into the Performance tab, and what I found changed how I debug frontend performance forever.

This post walks through exactly how I used the Performance tab to diagnose and fix that jank, and the techniques I now use for every performance investigation.

The Setup: A React List That Stutters

The app displayed a virtualized list of 10,000 items with complex row components. On scroll, the frame rate dropped from 60fps to around 20fps. I knew the virtualization library (react-window) was efficient, so the problem had to be in row rendering.

I opened DevTools, went to the Performance tab, and clicked the record button. I scrolled the list for about 3 seconds, then stopped. The result was a timeline full of red triangles.

info

Red triangles in the Performance tab indicate long tasks — tasks that block the main thread for more than 50ms. Anything over 50ms feels like jank to users.

Reading the Flame Chart

The flame chart shows a stack of function calls over time. The width of each bar is proportional to how long that function ran. My first instinct was to look for the widest bars. But wide bars aren't always the problem — sometimes they're just waiting for something else.

Instead, I looked for functions that appeared frequently and had high 'self time' (the time spent in the function itself, not its children). I switched to the 'Bottom-Up' tab, which aggregates functions across the entire recording.

The top function was something called `ExpensiveRowComponent.render`. It was taking 120ms total, with 80ms of self time. That was my suspect.

The problematic component — `expensiveFormat` ran on every render because the parent passed a new object each time.
// Simplified version of what I found
class ExpensiveRowComponent extends React.Component {
  render() {
    const { item, style } = this.props;
    // This function was called on every scroll!
    const formatted = expensiveFormat(item.data);
    return (
      <div style={style}>
        <p>{formatted}</p>
      </div>
    );
  }
}

The Real Culprit: Unnecessary Re-renders

The flame chart showed `ExpensiveRowComponent.render` appearing over and over, even for rows that hadn't changed. React was re-rendering 50+ rows per scroll event.

I opened React DevTools and checked the 'Highlight updates' option. Sure enough, every visible row flashed on scroll. The parent component was passing a new `item` object reference each time it rendered, breaking React.memo's shallow comparison.

The fix was simple: memoize the row component and stabilize the item references.

With `React.memo` and `useMemo`, the row only re-renders when `item.data` actually changes. The flame chart now shows zero unnecessary renders.
const Row = React.memo(({ item, style }) => {
  const formatted = useMemo(() => expensiveFormat(item.data), [item.data]);
  return (
    <div style={style}>
      <p>{formatted}</p>
    </div>
  );
});

// Parent passes same reference by using stable ids
const items = useMemo(() => data.map(d => ({ id: d.id, data: d })), [data]);
82%

Reduction in row render time after memoization

Beyond Flame Charts: Other Performance Tab Features

The flame chart gets all the attention, but the Performance tab has other views that reveal different problems.

The 'Summary' pane gives a high-level breakdown of what the browser spent time on: scripting, rendering, painting, system. In my case, scripting was 70% — a clear sign of too much JavaScript.

The 'Call Tree' tab shows which functions called which, useful for tracing the root cause of a long task. But my favorite is the 'Event Log' tab, which lists every single event (mouse move, scroll, paint) with timestamps. I've used it to find rogue `mousemove` listeners that fire 100 times per second.

The Case of the 60fps Drop on Hover

  1. 00:00User hovers over a card in a grid
  2. 00:02Frame rate drops to 20fps for 1 second
  3. 00:03Animation completes, fps returns to 60
  4. 00:05I record the hover with Performance tab
  5. 00:10Event Log shows 200 mouseover events in 200ms
  6. 00:12Found a third-party library attaching a listener to every element
  7. 00:15Switched to a single delegated listener on the container

Lesson

Event Log in the Performance tab helps you spot event storms that don't appear in the flame chart as long tasks but still cause jank by flooding the event queue.

Throttling CPU and Network for Realistic Profiles

Your MacBook Pro is not your users' phone. Always simulate a slower CPU before recording. In the Performance tab, set 'CPU throttling' to 4x or 6x slowdown. I use 6x to match a mid-range Android phone.

Also disable the cache (checkbox in the Network tab) to simulate cold loads. Without this, you might miss expensive JavaScript compile times that happen only on first visit.

I've caught dozens of issues that only appeared when CPU was throttled — things like heavy array operations that take 10ms on my machine but 200ms on a Moto G.

If you're not profiling with CPU throttling, you're not profiling for real users.

Common Mistakes When Using the Performance Tab

  1. 1Recording for too long — keep recordings under 10 seconds; long recordings are noisy and hard to analyze.
  2. 2Not clearing the console before recording — console.log calls block the main thread and pollute flame charts.
  3. 3Ignoring the 'Bottom-Up' tab — this is often the fastest way to find the most expensive functions.
  4. 4Forgetting to enable 'JS Profile' — in DevTools settings, check 'Enable advanced JavaScript profiling' to get detailed function names.
  5. 5Profiling production builds without source maps — you'll see anonymous functions everywhere. Use development build or deploy with source maps to error reporting.

Putting It All Together: A Debugging Checklist

  • arrow_rightReproduce the slow interaction consistently.
  • arrow_rightOpen Performance tab, set CPU throttle to 4x, disable cache.
  • arrow_rightRecord for 3–5 seconds of the interaction.
  • arrow_rightLook for red triangles (long tasks) and note their duration.
  • arrow_rightSwitch to Bottom-Up tab, sort by Total Time, find the top function.
  • arrow_rightClick the function to see its call stack and source location.
  • arrow_rightFix the function (memoize, debounce, defer, or cache).
  • arrow_rightRe-record to verify the fix reduced frame drops.
lightbulb

Pair this with React DevTools' Profiler tab for component-level timing. The Performance tab gives you system-level insights; React Profiler gives you component-level insights. Use both.

The Chrome DevTools Performance tab is the single most underused tool in frontend debugging. It's not just for finding jank — it can reveal memory leaks, layout thrashing, and even third-party script impacts. But it requires practice to read the flame chart and know which view to use for which problem.

Start with one investigation this week. Pick a page that feels slow, record it, and force yourself to find the top three long tasks. After three or four sessions, you'll start seeing patterns. And you'll never ship slow code again.

Frequently asked questions

How do I record a performance profile in Chrome DevTools?

Open DevTools (F12), go to the Performance tab, click the record button (circle), perform the interaction you want to measure, then click stop. Disable cache and set CPU throttling to 4x or 6x for realistic mobile results.

What is a 'long task' and why does it matter?

A long task is any JavaScript execution that takes more than 50ms on the main thread. The browser cannot respond to user input during a long task, causing jank. The Performance tab highlights them with red triangle icons in the timeline.

How do I tell which React component is causing a re-render in the flame chart?

In the flame chart, look for 'render' call stacks under the component's name. React DevTools also shows re-render counts. Combine both: use the Performance tab to see timing, and React DevTools to isolate which component re-renders too often.

Why does the flame chart show a lot of 'anonymous' functions?

Minified or production bundles often strip function names. Use source maps in development, or enable 'Enable advanced JavaScript profiling' in DevTools settings. For React, ensure you're using the development build when profiling.