Performance View: frame analysis and jank tracing in profile mode
3 min read
DevTools
| Bar / track | What it represents |
|---|---|
| UI thread bars | Time spent on build / layout / paint per frame |
| Raster thread bars | Time spent rasterising (GPU work, compositing) |
| Red bar | Dropped frame — total exceeded budget |
| Yellow bar | Frame near the limit |
| Vertical guide | The 16.67ms / 8.33ms budget line |
A frame can drop because the UI thread was too slow (build/layout) OR the raster thread was too slow (paint/compositing). Click a red frame → DevTools shows the breakdown.
Workflow — the frame analysis loop
# 1. Always profile mode
flutter run --profile
- Open DevTools → Performance.
- Press Record.
- Reproduce the laggy interaction (scroll, animation, page transition).
- Stop recording.
- Find the red bars on the timeline.
- Click into one — DevTools shows the Build / Layout / Paint breakdown.
- If UI thread is the culprit → CPU Flame Chart (Q61) to find the slow code.
- If Raster thread is the culprit → look at the Layers / Raster Cache views.
- Fix the hotspot.
- Re-run the same recording → confirm the bar is green now.
Common root causes by symptom
| Symptom | Where to look | Usual fix |
|---|---|---|
| Jank on first appearance | UI thread, especially build | Cache derived data; defer heavy work; precacheImage |
| Jank during scroll | UI thread per frame, then raster | ListView.builder; lighter item widgets; RepaintBoundary |
| Jank during animation | Raster thread spikes | RepaintBoundary around animated subtree; smaller dirty area |
| Jank on screen rotation / keyboard | UI thread | Avoid MediaQuery.of high in the tree; use targeted helpers |
| Jank after a state change | UI thread spike at one frame | Narrow rebuild scope (push state down, context.select) |
| Steady high frame times, no spikes | Both threads | Look for per-frame allocations (GC pressure); image decode on UI thread |
Useful tools alongside the Performance view
| Tool | When to flip it on |
|---|---|
Performance Overlay (MaterialApp.showPerformanceOverlay: true) | On-device, real-time graph of frame times — quickest sanity check |
| debugProfileBuildsEnabled | Records every widget's build time in the timeline |
| debugRepaintRainbowEnabled | Visualises repaints — pulsing colour = repainting too often |
| Memory profiler (Q62) | If perf issues correlate with growing memory |
| CPU Flame Chart (Q61) | Once you know when it janks, find what code caused it |
Common mistakes to avoid
❌ Measuring in debug mode
Debug adds assertions, layout verification, allocations — 5-10× slower.
✅ Always flutter run --profile
❌ Looking at average frame time only
A 99th-percentile spike is what users feel as jank. Look at the spikes, not the mean.
❌ Optimising before measuring
"I bet it's the X widget" → spend 2 days → wasn't even that.
Measure first, then fix.
❌ Optimising without re-measuring
Confirm the fix actually moved the timeline before declaring victory.
❌ Confusing UI-thread and raster-thread jank
Build-time fixes don't help GPU-bound jank. Read which thread is red.
❌ Profiling on the host machine
x86 desktop performance ≠ low-end ARM device performance. Profile on the slowest target device.
Interview follow-ups
-
What's the difference between UI thread jank and raster thread jank? UI thread runs build/layout/paint recording — anything in Dart code. Raster thread runs the actual GPU rendering. UI-thread jank is fixed by lighter Dart code (cache, defer, push state down). Raster-thread jank is fixed by lighter graphics work (fewer layers,
RepaintBoundary, smaller dirty regions). -
What is the 16ms budget and where does it come from? 60Hz screens refresh every 16.67ms. To deliver every frame on time, all work (build + layout + paint + raster + compositing) must finish within that window. 120Hz devices halve it to 8.33ms. The numbers come from human visual perception thresholds — anything missing the budget is felt as stutter.
-
What's a
RepaintBoundaryand when does it actually help? It gives a subtree its own compositing layer, so a repaint inside it doesn't dirty the parent. Use it around animated or frequently-changing subtrees — charts, video players, custom-painted widgets. Adding boundaries everywhere is not a win; each layer has cost. -
You see a red frame but the CPU profile looks light. What now? The bottleneck is probably the raster thread (GPU). Look at the raster thread bar — heavy paint can be caused by lots of
saveLayer, largeBackdropFilter, oversized images, or alpha-blended overlap. Switching to Impeller (Q58) often fixes shader-compile spikes specifically.
How helpful was this content?
Please sign in to rate this article.