Introduction
Profiling UI work in Flutter is essential for smooth mobile development. Two lightweight debug tools — the Performance Overlay and Repaint Rainbow — let you correlate visual repaint activity with frame times so you can find the widgets that cause jank. This tutorial shows how to enable both, read results, and reduce unnecessary repaints and rebuilds.
Setting Up The Performance Overlay
The Performance Overlay is the first place to look for frame-time problems. It shows two timeline graphs (UI vs raster) where each frame's cost is visualized; bars above the 16ms line indicate potential jank on 60fps displays. To enable it quickly during development, set the MaterialApp (or WidgetsApp) property showPerformanceOverlay to true, or use the PerformanceOverlay widget directly.
Example: enable overlay at app startup.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override Widget build(BuildContext c) => MaterialApp(
showPerformanceOverlay: true,
home: const Scaffold(body: Center(child: Text('Profiling'))),
);
}When the overlay is visible, watch for sustained spikes and their pattern. A single spike can be expensive layout, expensive build, or one-off work; repeated spikes tied to a UI animation or list scroll suggest repeated repaints or layout thrash.
Using RepaintRainbow To Visualize Repaints And Rebuilds
Repaint Rainbow is a debug flag that paints layers that are being repainted with cycling, high-contrast colors. It's not a production tool — it's a debug visualizer that immediately reveals how often parts of the UI repaint.
Enable it by setting debugRepaintRainbowEnabled = true from main(), which will color flashing areas as they repaint. Typical signals:
Persistent flashing on the same area: that subtree repaints very often.
Flashing large regions: you’re invalidating too big a paint area; consider RepaintBoundary to isolate children.
Flashing small widgets: those widgets are frequently marking themselves dirty — perhaps because setState is too broad or because animations are not cached.
Because repaint rainbow shows only paint operations, you should pair it with the Performance Overlay: a repaint spike on the overlay that matches wide flashing on the screen usually means raster work dominates performance. If the overlay shows UI-thread spikes against little repaint activity, look for expensive builds or layouts instead.
Profiling Strategies And Interpreting Results
1) Reproduce the scenario: enable the overlay and repaint rainbow, then perform the interaction (scroll, animate, tap) that reveals jank.
2) Observe correlations: does a long frame in the overlay coincide with a wide repaint rainbow flash? If yes, rasterization is expensive — image decoding, shader work, or large paint operations might be the cause.
3) Is the spike on the UI graph (often the top graph)? Then the cost is in layout, build, or synchronous computation. Use debugPrintRebuildDirtyWidgets (prints rebuilds) and the DevTools widget rebuild profiler to find frequently rebuilding widgets.
4) Narrow down the region: wrap suspect subtrees with RepaintBoundary to isolate repaint cost. If isolating reduces the flashing and the overlay's raster time goes down, you’ve found a paint-heavy subtree.
Minimizing Repaints And Rebuilds
Prefer const constructors and immutable widgets where possible so Flutter can short-circuit rebuilds.
Group independent, frequently-updated parts inside RepaintBoundary to limit repaint footprints.
Cache expensive draw results with RepaintBoundary + Layer caching or use PictureRecorder if you draw repeatedly.
Avoid calling setState on a parent when only a small child needs updating; instead move state down or use ValueListenable/Provider/Bloc to scope updates.
Consider using RenderObjects for highly optimized, custom painting when widgets are repeatedly painted with little change in tree structure.
Example: using RepaintBoundary for an expensive child.
class Counter extends StatefulWidget {
const Counter({super.key});
@override State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override Widget build(BuildContext context) => Column(children: [
GestureDetector(onTap: () => setState(() => count++), child: Text('$count')),
const RepaintBoundary(child: ExpensiveWidget()),
]);
}This keeps ExpensiveWidget from repainting when only the counter changes.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
The Performance Overlay and Repaint Rainbow form a fast, code-first feedback loop for diagnosing UI jank in Flutter mobile development. Use the overlay to spot frame-time spikes and Repaint Rainbow to see where paint work occurs. Combine those visuals with scoped state management and RepaintBoundary to reduce the repaint footprint and improve frame stability. Start by reproducing the problematic interaction, correlate overlay spikes with repaint flashes, then isolate and refactor the offending subtree.