CPU Flame Chart: identify slow calls and instrument Timeline events
3 min read
DevTools
The flame chart visualises call stacks sampled over time:
| What you see | What it means |
|---|---|
| Wider bar | Function (or chain) spent more wall-clock time |
| Bars stacked top-to-bottom | Caller → callee — deeper stack on top |
| Same function across many bars | Called many times — Σ width is total time |
| Bar interrupted by a different colour | Other 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
- Run the app in profile mode:
flutter run --profile. - Open DevTools → Performance (or CPU Profiler).
- Press Record.
- Reproduce the slow interaction.
- Stop recording.
- 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
| Pattern | Likely cause |
|---|---|
One huge build bar | A widget doing heavy work in build() |
Many small bars of _HashMap.[] / containsKey | O(n) List.contains in a loop — use a Set (Q3) |
jsonDecode dominating a frame | Parsing on the UI thread — move to Isolate.run / compute |
Image.decode in the chart | Decoding images on UI thread — use cacheWidth / precacheImage |
Repeated Theme.of(context) walks | Cached 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 stutters | Performance / Timeline (Q63) |
| What code took the time during a stutter | CPU Flame Chart |
| Memory growth / leaks | Memory profiler (Q62) |
| Network slowness | Network 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
-
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.
-
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.
-
What's the cost of
Timeline.startSync/finishSyncin 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. -
A widget rebuild seems to dominate the chart. Now what? Two angles: reduce how often it rebuilds (push state down, use
select, addRepaintBoundary) and reduce whatbuild()does (cache derived data in State, useconstconstructors, defer heavy work). Always measure after each change.
How helpful was this content?
Please sign in to rate this article.