CPU Flame Chart: identify slow calls and instrument Timeline events

Low PriorityAsked in ~40% of mid-level interviews

3 min read

DevTools

The flame chart visualises call stacks sampled over time:

What you seeWhat it means
Wider barFunction (or chain) spent more wall-clock time
Bars stacked top-to-bottomCaller → callee — deeper stack on top
Same function across many barsCalled many times — Σ width is total time
Bar interrupted by a different colourOther code interleaved — async pause, gc, frame boundary

It's a statistical profile: the engine samples the stack many times per second and counts samples. Wider doesn't guarantee more cost — just higher probability of being on-CPU.


Workflow

  1. Run the app in profile mode: flutter run --profile.
  2. Open DevTools → Performance (or CPU Profiler).
  3. Press Record.
  4. Reproduce the slow interaction.
  5. Stop recording.
  6. Inspect the flame chart for the slow window:
    • Sort by self-time or total-time.
    • Drill into the widest leaf — that's where to investigate first.
    • Look for surprises: String.fromCharCodes, _HashMap.[], jsonDecode — common hidden costs.

Instrumenting your own code — Timeline spans

import 'dart:developer' as developer;

Future<List<Profile>> loadProfiles() async {
  developer.Timeline.startSync('loadProfiles');
  try {
    final data = await api.fetchProfiles();
    final parsed = parseProfiles(data);
    return parsed;
  } finally {
    developer.Timeline.finishSync();
  }
}

// Or with arguments for filtering / context
developer.Timeline.timeSync('parse', () => parseProfiles(raw), arguments: {'n': raw.length});

These show as named bars in the DevTools timeline — much easier than finding random _internalImpl$5 frames.


Reading the chart — common patterns

PatternLikely cause
One huge build barA widget doing heavy work in build()
Many small bars of _HashMap.[] / containsKeyO(n) List.contains in a loop — use a Set (Q3)
jsonDecode dominating a frameParsing on the UI thread — move to Isolate.run / compute
Image.decode in the chartDecoding images on UI thread — use cacheWidth / precacheImage
Repeated Theme.of(context) walksCached in a parent — read once if used many times

When to use this vs the Performance View

You want to find…Use
When the app stuttersPerformance / Timeline (Q63)
What code took the time during a stutterCPU Flame Chart
Memory growth / leaksMemory profiler (Q62)
Network slownessNetwork panel (junior Q39)

Rule of thumb: Performance view tells you the frame is bad. Flame chart tells you which function made it bad.


Common mistakes to avoid

❌ Profiling in debug mode
   Debug has assertions, type checks, no AOT — everything is artificially slow.
   ✅ flutter run --profile

❌ Looking at the whole recording instead of a window
   Zoom into the slow frames in the timeline first, then read the flame chart for THAT window.

❌ Trusting wide bars without checking sample count
   A wide bar with very few samples might be noise. Compare runs.

❌ Forgetting to instrument your own code
   Without Timeline spans, you're reading dart:async / dart:ui frames everywhere.
   ✅ A handful of well-placed timeSync calls makes the chart immediately readable.

❌ "Optimising" without measuring twice
   Always re-profile after your fix to confirm it actually moved the needle.

Interview follow-ups

  1. What's the difference between sampled and instrumented profiling? Sampled (what DevTools' CPU profiler does): the runtime periodically captures the current stack — cheap, statistical, may miss short events. Instrumented: every function entry/exit is recorded — exact but heavy. Timeline spans you add yourself are a hybrid — explicit instrumentation for the code you care about.

  2. How do you decide whether a wide bar is a real problem? Compare against the frame budget (16.67ms at 60Hz). If the bar represents work that blocks a frame and exceeds budget, it's a real problem. If it's evenly spread across many frames, it might just be background work.

  3. What's the cost of Timeline.startSync / finishSync in production? In release mode, they're no-ops (compiled out). In debug/profile, the cost is small — a few microseconds. Safe to leave them in code; they only fire when DevTools is recording.

  4. A widget rebuild seems to dominate the chart. Now what? Two angles: reduce how often it rebuilds (push state down, use select, add RepaintBoundary) and reduce what build() does (cache derived data in State, use const constructors, defer heavy work). Always measure after each change.


How helpful was this content?

Please sign in to rate this article.