Rebuild performance and measurement
The performance impact of state management comes down to one question: how many widgets does a single change wake? The official performance guidance is concrete here:
- Use `const` constructors on widgets as much as possible, since they allow Flutter to short-circuit most of the rebuild work. The
flutter_lintspackage reminds you automatically. - Avoid repetitive and costly work in `build()` methods since `build()` can be invoked frequently when ancestor widgets rebuild.
- Avoid overly large single widgets with a large `build()` function; split them based on encapsulation but also on how they change. The docs add that when
setState()is called, all descendant widgets rebuild, so the call should be localised to the part that actually needs to change. - Prefer a `StatelessWidget` over a function for reusable pieces of UI.
- When building a large grid or list, use the lazy builder methods — only the visible portion is built.
| Library | Tool for narrowing the rebuild scope |
|---|---|
| Flutter (no library) | Pushing `setState` down, `ValueListenableBuilder`, `const`, `ListenableBuilder` |
| Provider | `Consumer` (with its `child` parameter), `Selector`, `context.select` |
| Riverpod | The `Consumer` widget, `provider.select(...)`, separate small providers |
| BLoC | `BlocSelector`, `buildWhen`, placing `BlocBuilder` deeper |
Measurement: don't optimise by guessing. DevTools' Performance view offers concrete options for finding rebuilds:
- Track Widget Builds — shows
build()method events in the timeline; the widget's name appears in the event. - Enhance tracing — options for more detailed tracing; the docs caution that frame times might be negatively affected while they are enabled.
- Track Layouts and Track Paints — layout and paint events of render objects.
A strong way to phrase the sequence in an interview: measure first, then narrow the rebuild scope, then measure again. An optimisation claim without a number is a guess.
// ❌ Bütün ekran hər dəyişiklikdə qurulur:
// - watch build-in yuxarısındadır
// - ağır widget-lər eyni metodun içindədir
// - const yoxdur
class DashboardBad extends StatelessWidget {
@override
Widget build(BuildContext context) {
final model = context.watch<DashboardModel>();
return Column(
children: [
HeavyChart(data: model.chartData),
ExpensiveMap(),
Text('Bildiriş: ${model.unreadCount}'),
],
);
}
}
// ✅ Yalnız dəyişən hissə qurulur:
// - ağır widget-lər const və ya kənarda
// - abunəlik yalnız lazım olan sahəyə
class DashboardGood extends StatelessWidget {
const DashboardGood({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Yalnız chartData dəyişdikdə qurulur.
Selector<DashboardModel, List<Point>>(
selector: (_, m) => m.chartData,
builder: (_, data, __) => HeavyChart(data: data),
),
const ExpensiveMap(), // heç vaxt rebuild olunmur
Selector<DashboardModel, int>(
selector: (_, m) => m.unreadCount,
builder: (_, count, __) => Text('Bildiriş: $count'),
),
],
);
}
}The same screen, two rebuild profiles.
The most common performance mistake is not in the state management but in subscribing too high: watching the whole model on the first line of build binds every part of the screen to a single change. It is the same mistake in all three libraries, and the same fix — push the subscription down the tree (Consumer, Selector, BlocSelector, small Consumer widgets in Riverpod).
📚 Sources and documentation
- Flutter performance best practicesofficialdocs.flutter.dev
The source of the const, costly-work-in-build(), setState scope and lazy builder recommendations.
- DevTools: Performance viewofficialdocs.flutter.dev
The **Track Widget Builds**, **Enhance tracing** and **Track Layouts/Paints** options.
- Profiling UI performanceofficialdocs.flutter.dev
- RepaintBoundary APIofficialapi.flutter.dev
For isolating the repaint area — a different phase from rebuilding.