In a Chrome DevTools performance trace, the main-thread flame chart shows one very wide bar whose children fill almost its entire width. What does a bar's width mean here, and how do the Self time and Total time figures tell you which function is actually worth optimizing?
answer
- two numbers tell different stories
- wide parent, thin culprit
- cost inside the frame vs below it
- aggregate across every call site
basics
~20 sWidth in a flame chart is wall-clock duration and depth is call nesting. Total time counts a function plus everything it called; Self time counts only its own work. Optimize where Self time concentrates, not the wide parent containing it.
solid answer
~50 sIn the main-thread flame chart, the horizontal axis is time: a bar's width is how long that call took, and the bars stacked beneath it are the calls it made. So a wide bar whose children fill it is not the hot spot — it is a container. Its **Total time** (itself plus everything it called) is large while its **Self time** (work done in its own frame) is near zero, and optimizing it directly buys nothing. To find the real cost I switch to the Bottom-Up view, which aggregates Self time per function across every call site — that is where a small utility called ten thousand times inside a loop finally shows up, even though no single one of its bars is wide enough to see. I also keep in mind that the JavaScript profiler samples the stack, so very short calls can be attributed imprecisely, and that without source maps the names are minified. Panel labels shift between Chrome versions; the Self/Total distinction does not.
code
javascript · 13 linesfunction renderRows(rows, container) {
// Total time is huge; Self time is tiny - this frame is a container.
for (const row of rows) {
const el = document.createElement('div');
el.textContent = formatRow(row);
container.appendChild(el);
}
}
function formatRow(row) {
// Called once per row: each bar is invisible, the summed Self time is not.
return row.values.map((v) => v.toFixed(2)).join(' | ');
}go deeper
Know that the horizontal axis is time and the stack below a bar is what it called, and that a wide bar can simply be a container for the real cost.
Explain the Self-versus-Total distinction precisely and say which view aggregates Self time per function across all call sites, then use it to name the code you would change.
Demonstrate that you know the chart's limits — sampling imprecision, minified names, cost attributed to framework frames — and that you turn a hot leaf into a decision about calling it less, not just making it faster.
Be ready to argue where profiling effort pays off at all: which shapes of trace indicate a fixable hot spot versus a product that simply does too much, and what that implies for scope rather than for code.
## What a flame chart actually plots A main-thread flame chart has two axes and they mean different things, which is where most misreadings start. - **Horizontal = time.** A bar's left edge is when the call started, its width is how long it ran in wall-clock terms. Width is *not* call count, *not* memory, *not* importance. - **Vertical = call depth.** A bar drawn directly beneath another is a function that the one above called. In Chrome DevTools the stack grows downward, so callers sit on top and the leaves are at the bottom of each column. Because of that layout, a wide bar near the top says only "this call and everything under it took a long time". A root frame for an event handler will usually be the widest thing on screen, and it is almost never the thing you edit. ## Total time versus Self time Every frame carries two durations: - **Total time** — the whole subtree: this frame plus every function it called, transitively. - **Self time** — the time the CPU spent in this frame's own code, excluding its callees. Self time is what you can remove by rewriting that function. Total time tells you which branch of the tree to walk into. A typical pattern: ```js function renderRows(rows) { // Total ~900 ms, Self ~4 ms for (const row of rows) { container.appendChild(formatRow(row)); } } function formatRow(row) { // Self time concentrates here return row.values.map(v => v.toFixed(2)).join(' | '); } ``` `renderRows` looks catastrophic in the flame chart and is nearly free in itself. The cost lives in ten thousand tiny `formatRow` bars that are each a fraction of a pixel wide. ## Call Tree versus Bottom-Up This is exactly why the details pane offers more than one aggregation of the same recording: - **Call Tree** aggregates top-down from the roots, so you can follow the expensive branch downward from a root frame to its leaves. - **Bottom-Up** aggregates by function, summing Self time across every call site in the recorded range. A helper invoked from six places, none of them individually alarming, jumps to the top of this list. - **Summary** gives the coarse split of the selected range by activity category, which is a good first look before you go hunting for a specific function. A reliable habit: select the range you care about, glance at Summary to confirm the time really is scripting, then read Bottom-Up to find where Self time concentrates, then use Call Tree or the flame chart to learn *who calls it* — because the fix is often "call it less" rather than "make it faster". ## Three shapes you will actually see 1. **One wide leaf.** A single function doing genuinely heavy work — a sort, a parse, a serialization. The rare easy case: optimize it or move it off the critical moment. 2. **A wide parent over a repeated tiny leaf.** The loop pattern above. The winning fix is usually algorithmic (do the work once, batch it, or do less of it) rather than micro-optimizing the leaf. 3. **A broad plateau of many different small frames.** No single hot spot; the page is simply doing a lot. This is the shape that says "remove work" — defer, split, or delete a feature — because there is nothing to make faster. ## Where the flame chart misleads - **Sampling.** The JavaScript profiler periodically samples the call stack rather than instrumenting every call, so very short functions can be attributed imprecisely and tiny frames may appear or vanish between recordings. Trust the shape and the aggregates, not a single 0.3 ms bar. - **Minified names.** Without source maps loaded you get one-letter functions and `(anonymous)`; load them before you try to interpret a production trace. - **Framework and engine frames.** Cost attributed to a library frame is frequently *your* callback running inside it. Walk down the stack until you reach code you own before concluding the library is slow. - **Non-JavaScript work.** Wide bars in the style, layout and paint categories are not JavaScript at all, and hunting for a function to optimize inside them is a category error — those are caused by what the script asked the browser to do. ## From the chart to a fix The flame chart's job ends at "this is where the time goes". The decision that follows is a different one: can the work be removed, deferred to a later moment, done once instead of repeatedly, or moved off the main thread? Answer that with the call relationships the chart gave you, then change one thing and record again.
- A helper never appears as a wide bar anywhere, yet it is the biggest cost in the trace. How does that happen and how do you find it?It is called many times, so its cost is split across thousands of sub-pixel bars that the flame chart cannot show individually. The Bottom-Up view sums Self time per function across every call site in the selected range, which surfaces it immediately. Once found, the fix is usually to call it fewer times — hoist it out of a loop, memoize, or batch — rather than to micro-optimize its body.
- Why can you not simply optimize the widest bar in the chart?The widest bar is normally a root or container frame whose width is the sum of its subtree. Its own Self time is near zero, so rewriting it removes nothing. Width tells you which branch to descend into; Self time tells you where the CPU actually was. Read them together, then look at who calls the hot leaf, because calling it less often is usually the larger win.
- What should you check before trusting a single narrow bar you see in a production trace?Two things. First, whether source maps are loaded — minified names and `(anonymous)` frames make attribution guesswork. Second, that the bar is not an artefact of sampling: the JavaScript profiler samples the stack at intervals, so very short calls can be attributed imprecisely. Re-record and check whether the aggregate in Bottom-Up agrees before building a theory on one thin bar.
saying these in an interview costs you the question
- Reads bar width as the number of calls
- Optimizes the widest bar, ignoring its near-zero Self time
- Assumes the library frame is slow, not the callback inside it
- Trusts one sub-millisecond bar from a sampling profiler
- Reads a minified production trace without loading source maps