Reading a Flame Chart

Your dashboard takes three seconds to render and everyone has a theory. It’s the date formatting. No, it’s the big sort in the middle. Definitely the JSON parse. So you spend an afternoon making the sort twice as fast, ship it, and the dashboard still takes three seconds. The sort was two percent of the time. You optimised a rounding error.

Guessing which line is slow is the classic way to burn a day. A profiler ends the argument. It records where the CPU actually spent its time and draws it as a flame chart, and once you can read that chart the three-second mystery collapses into a short to-do list. That’s the whole skill here, and it’s evergreen: the chart reads the same whether it came from a browser’s performance panel or a server-side Node profile.

The picture comes from sampling

Before you trust a flame chart, it helps to know where it comes from, because that tells you what it can and cannot say. Most CPU profilers you will meet are sampling profilers, and the idea is almost crude. Many times a second, the profiler interrupts your program, looks at the call stack (which function is running right now, who called it, all the way down to the root), writes that stack down, and lets the program carry on. Do that a thousand times a second for a few seconds and you have thousands of snapshots of what the CPU was busy with.

The important word is statistical. Nobody put a stopwatch on any single function. A function that was genuinely busy gets caught in a lot of snapshots. A function that barely ran gets caught in few, or none. The profiler just tallies how often each one was spotted, and if you sample often enough, those tallies are a faithful map of where the time went.

the sampler interrupts many times a second and writes down the stacksamplert1t2t3t4t5t6leaf (on CPU)formatformatdrawformatformatdrawcallerupdateupdateupdateupdateupdateupdatetally: how many samples caught each function running (its leaf count)format4 of 6draw2 of 6more samples on a function means a wider bar in the chartrare, bursty work between ticks can be under-counted or missed entirely
A sampling profiler grabs the call stack at each tick. The function caught on the CPU most often becomes the widest bar.

V8’s profiler (the one behind Chrome and Node) samples about once a millisecond by default, though that interval is configurable and not a law of nature. Other engines pick their own defaults. What matters is the trade: sample more often and you catch finer detail at the cost of overhead, sample less often and you miss short-lived work.

Reading the chart: two axes and a rule

The flame chart takes those stacked snapshots and draws them. It has exactly two axes, and they are probably not the two you would guess.

The x-axis is time, or equivalently the number of samples a function accounts for. Wider means more time. That part is intuitive.

The y-axis is call-stack depth. It is not time, and it is not importance. The bottom bar is whatever was running at the root: an event handler, the top of a request. Sitting directly on a bar are the functions it called. On top of those, the functions they called. Height is only “how deep in the call stack you are”, nothing else. A tall tower is a deep chain of calls. It is not, by itself, slow.

Put the two together and the chart reads like this. Each bar is one function call, a frame. Its width is the time spent in that function plus everything it called. The frames stacked above it are its callees. So you read down a column to see who called whom, and you scan across to see where the width, the time, actually sits.

wide bar, bare top, narrow ancestorsthe hot spotstack depthescapeHtmlformatRowspaintserializesendrendersavehandleClicktime (wider means more of it)
A flame chart: width is time, height is call-stack depth. The hot spot is a wide bar with a bare top, high above narrow ancestors.

Look at that chart for a second. handleClick is the widest bar of all, because it contains everything. That does not make it the thing to fix. Almost all of its width is inside render, and most of render is inside formatRows, and most of formatRows is inside escapeHtml, which has almost nothing on top of it. That bare-topped bar is where the CPU actually is. Which brings us to the one distinction that matters most.

Self time is the whole game

Two numbers hang off every frame, and mixing them up is the single most common way to misread a profile.

Total time is the width of the bar: the function plus everything it called. Self time is the slice of that bar with nothing stacked on top, the function’s own work, not counting its callees.

In sampling terms it is clean. Total time is how many samples had this function anywhere in the stack. Self time is how many samples had it at the very top, on the CPU, with no child above it. A frame with a huge total but a tiny self is not doing much itself. It is standing there waiting for something it called. The time is real, but the culprit is higher up the stack.

total time (bar plus callees)self time (own work only)loadDashboardself 2%buildRowsself 4%escapeHtmlself 90%wide total AND wide self: this is the function actually burning CPUthe parents are just waiting on their callee; do not optimise them
Total time is the whole bar; self time is the function's own work. Parents can have huge total and near-zero self. Optimise the big-self frame.

Here is the mistake in the wild. You open the profile, see loadDashboard sitting at the top with 98% total time, and set about “optimising” it. But loadDashboard’s own code is three lines. Its self time is basically zero. All 98% is inside a function it called, and inside that, another. So you chase the width upward, frame by frame, until you reach a bar that is wide and has little or nothing on top of it. That bar’s self time is high. That is the function actually doing the work. That is what you optimise.

Try it on a real (if hand-built) profile. Hover any frame to see its self and total time. Then press Find the hot spot and watch it deliberately skip the frame with the biggest total, the tempting one, and land on the frame with the biggest self time, the real one:

interactiveA flame chart you can read (static profile, self vs total)

Shapes you learn to recognise

After you have read a few profiles, the shape starts telling you what kind of problem you have before you read a single label.

wide plateauhandleroneBigFunctionone function dominatesthe best case: a singleclear target to fixtall thin spikea deep call chaindepth is usually fine;only worry if it is wide toomany small barshot loopone small cost, paid N timesdeath by a thousand cuts:do it fewer times, not faster
Three flame-chart shapes and what each usually means: one dominant function, a deep call chain, or the same small cost paid over and over.

A wide plateau. One bar, or one short stack, spread across most of the width and flat on top. A single function dominates. This is the best case to find, because there is one clear target and fixing it moves the whole number.

A tall, thin spike. A narrow tower going way up. That is a deep call chain or recursion. Depth on its own is not a problem (frameworks nest deep and it is completely fine), so read the width, not the height. It only matters if the tower is also wide, or if it keeps growing on each run, which can point at runaway recursion.

Many small bars, repeated. A picket fence of narrow frames, all roughly the same, side by side. No single bar looks guilty, yet together they are most of your time. This is death by a thousand cuts: some small cost paid once per item, times ten thousand items. The fix is rarely to make the small thing faster. It is to do it fewer times. If you have met the N+1 query problem, this is its CPU-side cousin: each call is cheap, the count is the bug.

From chart to to-do list

The point of all this is a repeatable loop, not a staring contest with a colourful picture. It goes like this.

1 · RECORD
Reproduce the actual slow thing. The profile is only as honest as what you captured.
2 · RANK
Sort bottom-up by self time. The top rows are your candidates, biggest payback first.
3 · CONFIRM
Check the frame is on the path the user waits on. A wide idle bar is not worth fixing.
4 · FIX & RE-MEASURE ↻
Change one thing. Re-record. If the total moved, keep it. If not, revert and return to step 2.
The profiling loop: record a real interaction, rank by self time, confirm it matters, fix one thing, then measure again.

The single discipline that separates this from guessing is step 4. You always re-measure. A profiler is not only for finding the problem, it is for proving you fixed it. Plenty of “optimisations” shrink the bar you were staring at and leave the total untouched, because the time just moved somewhere else. The chart is your before-and-after photo. Trust it over your instinct about what should have worked.

This is exactly where the previous lesson landed: profile a real workload before you reach for a microbenchmark, and let the flame chart decide what deserves a benchmark at all. Actually fixing the hot spot it points at, with everything the pipeline chapter taught you, is the subject of optimisation in practice.

Flame graph or flame chart?

You will hear both terms, often for the same picture, and the difference is worth thirty seconds because it changes what the x-axis means.

flame chartx-axis is time, frames kept in orderlooptickticktickticktime →four separate calls, you see the loopflame graphx-axis is total time, identical stacks mergedlooptick (merged x4)restby total timeone wide bar, you see the total cost
Same samples, two layouts. A flame chart keeps time order so you see the sequence. A flame graph merges identical stacks into one bar so you see totals.

A flame chart keeps time order: the x-axis is the clock, so the same function called four times shows up as four separate bars, and you can see loops and the sequence of what ran when. A flame graph throws time away and merges every identical stack into one bar, sorted so the widest (most expensive) sits together, which answers “where did the total time go” at a glance but loses the story of when. Browser performance panels default to the time-ordered chart. Many server-side tools give you the aggregated graph. Same samples underneath, two questions: the chart answers when, the graph answers how much.

Where the charts come from

In the browser, Chrome’s Performance panel records a flame chart of the main thread while you interact with the page, and its bottom-up and call-tree tabs give you the same data as a self-time ranking. One honest detail: that panel blends sampled JavaScript stacks with instrumented browser events like layout and paint, which is why you also see rendering work in there, not only your functions. Firefox and Safari ship their own profilers with the same concepts under different labels.

On the server, run Node with --cpu-prof and it writes a .cpuprofile file you can drag straight into that same DevTools panel, or reach for a tool like 0x or clinic flame that renders an aggregated flame graph for you. The engine and the tools differ. The reading skill is identical everywhere: find the wide bars with bare tops, on the path that matters.

Summary

  • A profiler records where the CPU actually spent time; a flame chart draws it. Stop debating which line is slow and go look at one.
  • The picture is built by sampling: the profiler grabs the call stack many times a second (about once a millisecond by default in V8, and configurable) and counts. It is statistical, and it tells you where time went, not why.
  • Two axes. The x-axis is time (bar width). The y-axis is call-stack depth (who called whom), not importance. A tall tower is deep, not necessarily slow.
  • Self time, not total time, points at the culprit. Total is the bar plus its callees; self is the bar’s own work. Chase the width upward to a wide bar with a bare top, or let a bottom-up view rank self time for you.
  • Learn the shapes: a wide plateau (one clear target), a thin spike (deep calls, usually fine), and many small repeated bars (do it fewer times, not faster).
  • The loop: record a real interaction, rank by self time, confirm it is on the critical path, fix one thing, re-measure. Always re-measure.
  • A flame graph merges identical stacks by cost; a flame chart keeps them in time order. Same data, different question.

What this means for your code: do not optimise from intuition or from the loudest opinion in the room. Record the actual slow path, read the flame chart for high self-time frames that sit on the critical path, fix the top one, and re-profile to prove it moved the number. More often than not, the real hot spot is nowhere near where the team guessed.