Skip to content

Performance Tab: Reading Flame Graphs

advanced16 min read

Stop Guessing, Start Measuring

Here's how most performance debugging goes: "the page feels slow" → sprinkle console.time around random functions → find something that takes 50ms → optimize it → the page still feels slow. Sound familiar? The problem isn't your optimization skills — you optimized the wrong thing. Flame graphs show you exactly where time is spent, in what order, and at what stack depth. No guessing required.

Mental Model

A flame graph is a map of time. The x-axis is a timeline — left is earlier, right is later. The y-axis is the call stack — bottom is the entry point, each bar stacked on top is a function called by the one below it. The width of each bar is how long that function was on the stack. Wide bars are expensive. Narrow bars finish quickly. You read it like a topographic map: find the widest bars, follow the stack downward to find what they called, and you've found your bottleneck.

Recording a Performance Trace

Open the Performance panel (Cmd+Opt+I → Performance tab) and click the record button or press Cmd+E. Perform the action you want to profile, then click stop. Easy enough — but a few details make the difference between a useful trace and a misleading one:

Key recording tips:

  • Start clean: Close other tabs, disable extensions (they inject scripts), use an Incognito window
  • CPU throttling: Use the gear icon to add 4x or 6x slowdown — this simulates a mid-range mobile device and makes bottlenecks visible that you'd miss on a fast dev machine
  • Network throttling: Set to "Fast 3G" or "Slow 3G" to see how network-dependent the experience is
  • Keep recordings short: 3-5 seconds of the specific interaction. Long recordings produce massive traces that are hard to navigate
Profile in production builds

Development builds of React include extra checks, warnings, and devtool hooks that massively inflate scripting time. Always profile against a production build (next build && next start or equivalent). React's dev mode can be 2-10x slower than production — you'll chase phantom bottlenecks if you profile dev mode.

Anatomy of the Performance Panel

Once you stop recording, you're greeted by what looks like a wall of data. Don't panic. Here's what you're looking at — several horizontal tracks stacked vertically:

TrackWhat It Shows
OverviewMiniaturized view of CPU activity (green = paint, blue = load, yellow = scripting, purple = rendering). FPS bar on top, NET bar below
FramesEach frame rendered — green for on-time (≤16ms), red for dropped. Hover for exact duration
MainThe flame chart of the main thread — every function call, every layout, every paint event
RasterWork done by rasterizer threads (tiling, decoding images)
GPUCommands sent to the GPU for compositing
NetworkRequest waterfall timeline (aligned with the main thread timeline)
TimingsUser Timing marks (performance.mark/measure), FCP, LCP, DCL events

The Main track is where 90% of your analysis happens. This is the flame chart.

Quiz
You record a Performance trace and see large yellow blocks dominating the Overview strip. What category of work is consuming time?

Reading the Flame Chart

Width = Time, Depth = Stack

This is where the magic happens. Every bar in the flame chart represents a function call, and you only need two rules to read it:

  1. Width is proportional to how long the function was on the stack (including time spent in functions it called). A bar spanning 200ms means that function (and its callees) took 200ms.
  2. Depth shows the call stack. The topmost bar is the outermost caller. Each bar below is a function called by the one above it. The bottommost bars (leaves of the call tree) are where CPU time was actually spent.
┌───────────────── Task (800ms) ──────────────────┐
│ ┌──── handleClick (780ms) ─────────────────────┐ │
│ │ ┌─ fetchData (200ms) ─┐ ┌─ processData (550ms) ─┐ │ │
│ │ │ ┌─ fetch (195ms) ─┐ │ │ ┌─ sort (400ms) ────────┐ │ │ │
│ │ │ └────────────────┘ │ │ ┌─ filter (100ms) ─┐│ │ │ │
│ │ └───────────────────┘ │ └───────────────────┘│ │ │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

In this simplified trace: handleClick took 780ms total. Inside it, fetchData took 200ms and processData took 550ms. Inside processData, sort is the real culprit at 400ms. That's your optimization target.

Color Coding in the Main Thread

ColorCategoryExamples
YellowScriptingFunction calls, event handlers, timers, microtasks
PurpleRenderingRecalculate Style, Layout, Update Layer Tree
GreenPaintingPaint, Composite Layers
GraySystem/IdleIdle time, GC (Minor GC, Major GC)

The Red Corner: Long Tasks

See a red triangle in the top-right corner of a task? That's the browser's way of saying "this one's a problem." Any task exceeding 50ms gets this marker — they block the main thread long enough that user input cannot be processed, causing perceptible lag. Long tasks are the primary contributor to poor INP (Interaction to Next Paint) scores.

Quiz
A flame chart shows a task with a red corner in the top-right. What does this indicate?

Bottom-Up, Call Tree, and Event Log

Below the flame chart, you'll find three analysis tabs. Same data, three different lenses — and picking the right one saves you a ton of time:

Bottom-Up — Groups time by the leaf functions (where CPU time was actually consumed). Sorts by "Self Time" — time spent in the function itself, excluding callees. This answers: "Which function consumed the most raw CPU time?"

Call Tree — Top-down view starting from the root tasks. Shows the complete call hierarchy with time attribution. This answers: "What was the execution flow and how did time distribute across branches?"

Event Log — Chronological list of every event in the selected time range. Shows start time, duration, and category. This answers: "What happened in what order?"

Self Time vs Total Time

Total Time includes time spent in all called functions. Self Time is only time in the function itself. A function with 500ms Total Time but 2ms Self Time is just a coordinator — the real work happens in its callees. A function with 500ms Self Time is doing the heavy lifting directly. Always sort by Self Time in Bottom-Up view to find the real bottleneck.

Quiz
In Bottom-Up view, a function shows 300ms Total Time but only 5ms Self Time. Where should you look for the optimization opportunity?

Finding the Critical Path

Here's the skill that separates people who look at flame graphs from people who actually fix performance problems. The critical path is the longest sequential chain of work that determines total duration. In a flame graph:

  1. Select the long task you want to analyze
  2. In the flame chart, find the widest bars at each stack level — these are the dominant callees
  3. Follow the widest bars downward — this trace from root to leaf is your critical path
  4. The leaf with the most Self Time is your primary optimization target

Pattern: Layout Thrashing in a Flame Graph

Layout thrashing appears as alternating purple (Layout) and yellow (Script) bars in rapid succession:

┌─ Script: read offsetHeight ─┐┌─ Layout ─┐┌─ Script: write style ─┐┌─ Layout ─┐
└────────────────────────────┘└──────────┘└───────────────────────┘└──────────┘

Each read of a geometric property (offsetHeight, getBoundingClientRect) after a write forces a synchronous layout. The fix: batch all reads first, then all writes.

Pattern: Forced Reflow Markers

DevTools marks forced reflows with a red warning triangle and the label "Forced reflow is a likely performance bottleneck." Clicking the Layout event shows exactly which line of code triggered it in the call stack.

Quiz
You see alternating purple (Layout) and yellow (Script) bars repeating 20 times in a flame graph. What is happening?

User Timing API: Custom Marks in the Flame Graph

Flame graphs are great, but all those anonymous function calls can blur together. The User Timing API lets you inject your own named markers into the timeline, so you can spot your business logic phases instantly:

// Mark the start of an operation
performance.mark('data-processing-start');

// Do expensive work
const processed = transformData(rawData);

// Mark the end
performance.mark('data-processing-end');

// Measure the duration between marks
performance.measure('data-processing', 'data-processing-start', 'data-processing-end');

The performance.measure() call creates a named bar in the Timings track that aligns with the corresponding flame chart activity below. This is invaluable for marking business-logic phases ("data fetch", "render product list", "hydration") that are otherwise invisible in a sea of anonymous function calls.

Quiz
You add performance.mark() and performance.measure() around a suspected slow operation, but the measure shows only 5ms while the user perceives a 500ms delay. What's the most likely explanation?

The Frames Track: Spotting Dropped Frames

Want to know exactly when your app jitters? The Frames track shows each rendered frame as a colored block:

  • Green: Frame completed within budget (≤16.6ms for 60fps)
  • Yellow: Frame was partially delayed but still rendered
  • Red: Dropped frame — the browser could not produce a frame in time

Click a red frame to highlight the corresponding time range in the Main thread. The long task within that range is what caused the frame drop. For smooth interactions, every frame should be green.

Correlating Frames with INP

INP (Interaction to Next Paint) measures the delay between a user interaction and the next visual update. In the flame chart:

  1. Find the input event (Event: pointerdown, Event: click)
  2. Trace from the event through all synchronous work until the next paint
  3. The total duration from event to paint is approximately the INP for that interaction

If this exceeds 200ms, the interaction fails the INP threshold.

Practical Workflow: Profiling a Slow Interaction

Let's put it all together. When a user says "this button feels slow," here's your playbook:

  1. Open Performance panel with CPU throttling at 4x
  2. Click Record, perform the slow interaction, click Stop
  3. Zoom to the interaction in the Overview strip — click and drag to select just that time range
  4. Find the long task — look for the red-cornered bar on the Main thread
  5. Drill into the flame chart — follow the widest bars downward to find the critical path
  6. Check Bottom-Up — sort by Self Time to find the leaf function consuming the most CPU
  7. Identify the category — Is it scripting (algorithm)? Rendering (layout thrashing)? Painting (complex layers)?
  8. Fix and re-record — Apply the fix, record again, compare durations
Key Rules
  1. 1Width = time, depth = call stack. The widest bars at the deepest level are your optimization targets.
  2. 2Yellow = scripting, purple = rendering, green = painting. The dominant color tells you the category of bottleneck.
  3. 3Red corner = long task (> 50ms). Every long task is a potential INP failure — break them into smaller chunks.
  4. 4Self Time (not Total Time) identifies where CPU cycles are actually consumed. Sort Bottom-Up by Self Time.
  5. 5Layout thrashing appears as alternating purple/yellow bars. Fix with read/write batching.
  6. 6Always profile production builds with CPU throttling enabled — dev mode gives misleading results.
  7. 7Forced reflow markers (red triangle on Layout events) point to the exact line causing synchronous layout.
Interview Question

Q: Walk me through how you'd use the Performance panel to diagnose a janky scroll on a page.

A strong answer: (1) Record a trace with CPU throttling at 4x while scrolling. (2) Look at the Frames track for red (dropped) frames. (3) Zoom into a dropped frame and examine the Main thread — check if the frame took >16ms. (4) Identify what's dominating: if yellow (scripting), find the scroll/intersection observer handler in the flame chart. If purple (rendering), look for Layout events triggered by JavaScript (layout thrashing). If green (painting), check for non-composited animations or excessive paint regions. (5) Check the Layers panel for non-composited scrolling or too many layers. (6) Fix the bottleneck: defer scroll handlers with requestAnimationFrame, promote scrolling layers to the compositor, or reduce paint complexity.