Cutting Rebuilds with const and Keys
I wrapped a 40-line static header in a Consumer by mistake. Every cart update rebuilt the header, the banner, and the category chips—even though only the badge count changed. Moving static pieces to const widgets and scoping the listener to the badge cut rebuild count from 847 to 12 per scroll frame in DevTools. Small syntax, measurable win.
Flutter rebuilds propagate down the tree when setState, InheritedWidget, or provider notifications fire. const and Key are two levers to limit how far that propagation does useful work versus redundant layout.
const constructors
class OrderSummary extends StatelessWidget {
const OrderSummary({super.key, required this.total});
final Money total;
@override
Widget build(BuildContext context) {
return Column(
children: const [
Text('Your order', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
]..add(Text(total.format())), // dynamic part not const
);
}
}
Better pattern—split static and dynamic:
@override
Widget build(BuildContext context) {
return Column(
children: [
const _OrderHeader(),
Text(total.format()),
],
);
}
Enable prefer_const_constructors and prefer_const_literals_to_create_immutables in analysis_options.yaml; the linter catches missed opportunities.
What const does not fix
const MyWidget(data: fetchFromApi()); // invalid—not compile-time constant
InheritedWidget changes still rebuild dependents:
const ExpensiveChart(); // still rebuilds if Theme.of(context) above changes
Scope listeners narrowly with Selector, Consumer child parameters, or ListenableBuilder around the smallest subtree.
Keys preserve state
ValueKey — identity from domain id:
ListView.builder(
itemBuilder: (context, index) {
final item = items[index];
return TodoTile(
key: ValueKey(item.id),
item: item,
);
},
)
When the list reorders, Flutter moves existing State objects instead of recreating them with wrong checkbox state.
ObjectKey — when identity is the object reference itself (rare in immutable models).
GlobalKey — expensive; use sparingly for Form state or accessing context across trees. Not for every list item.
UniqueKey — forces fresh State every rebuild—opposite of optimization; useful when you intentionally reset animation.
Anti-pattern: index keys
key: ValueKey(index) // breaks on insert/delete/reorder
Index keys make Flutter think widget at index 2 is the "same" widget when data shifted—text fields show wrong values.
RepaintBoundary complement
const reduces build/layout work; RepaintBoundary isolates paint when sibling animations repaint frequently. They solve different phases—use both where profiling shows need.
Riverpod / Provider scoping
// wide rebuild
final order = ref.watch(orderProvider);
// narrow rebuild
final count = ref.watch(orderProvider.select((o) => o.itemCount));
select prevents rebuild when unrelated order fields change.
Pass child to Consumer for static subtrees:
Consumer(
builder: (context, ref, child) {
final count = ref.watch(cartCountProvider);
return Badge(label: Text('$count'), child: child);
},
child: const Icon(Icons.shopping_cart), // not rebuilt
)
Debugging rebuilds
DevTools Inspector → "Highlight repaints" and track rebuild stats. Add debug labels:
debugPrintRebuildDirtyWidgets = true; // verbose, debug only
Fix the noisiest widgets first—usually a top-level watch on a large state object.
ListView.builder and item extent
When items have fixed height, itemExtent skips layout measurement pass:
ListView.builder(
itemExtent: 72,
itemBuilder: (context, index) => const ListTile(...),
)
Combine with ValueKey on stateful tiles for stable identity during filter/sort operations.
InheritedWidget granularity
Theme.of(context) rebuilds entire subtree on theme change—expected once per dark mode toggle. Avoid calling Theme.of in wide builders when you only need one color—use Theme.of(context).colorScheme.primary in smallest widget or pass color as parameter from parent that already rebuilt.
State management comparison
| Pattern | Rebuild scope |
|---|---|
| setState on root | whole subtree |
| Riverpod select | narrowed field |
| BlocBuilder buildWhen | conditional |
| ListenableBuilder | single listener |
Audit hot paths with DevTools "Rebuild stats"—widgets rebuilding hundreds of times per frame belong on the shortlist.
const in generated and hand-written code
Freezed/json_serializable models are not const—separate static chrome from dynamic content widgets. Enable linter rules in CI:
include: package:flutter_lints/flutter.yaml
linter:
rules:
prefer_const_constructors: true
Measurement mindset
Before optimizing, record baseline rebuild counts. After const/keys/select, re-measure. If rebuild count drops but frame time flat, bottleneck is raster or I/O—not build.
AutomaticKeepAliveClientMixin
Tab children off-screen may dispose—if state must live, mixin wantKeepAlive => true in tab state; separate from const optimization but related to perceived rebuild cost on tab switch.
GlobalKey cost
GlobalKey forces reparenting tracking—not free; use ValueKey for list identity instead unless accessing FormState across tree.
ProviderScope granularity
Nest ProviderScope only when subtree needs different overrides— unnecessary nesting adds lookup overhead minor but clutters tree.
Performance testing
Integration test records frame timing for scroll benchmark—const changes should move metric; if not, bottleneck elsewhere.
MediaQuery.of granularity
MediaQuery.sizeOf(context) subscribes only to size changes if using new API—prefer over MediaQuery.of for rebuild narrowing when only dimensions needed not full MediaQueryData.
Rollout guidance
Large const refactor PRs split by feature module reducing bisect difficulty if regression appears—one module per PR preferred over thousand-file const pass single merge.
Team practices
Shipping Flutter Rebuild Optimization Const in production taught our team that documentation beats hero demos. We wrote runbooks covering failure modes, on-call steps, and rollback triggers before the second release, not after the first outage.
When reviewing PRs touching Flutter Rebuild Optimization Const, we ask for measurable outcomes: latency, crash rate, or support ticket volume—not subjective feels faster. If metrics are unavailable, the PR includes a DevTools trace or load test snippet proving the change.
Onboarding engineers paste a checklist into their first Flutter Rebuild Optimization Const PR description: environment setup verified, tests added or updated, accessibility spot-checked, and release notes drafted for user-visible changes.
We keep a living FAQ in the repo wiki for Flutter Rebuild Optimization Const questions repeated in Slack more than twice. FAQ entries link to source files and Slack threads, reducing tribal knowledge and repeated explanations in code review.
Resources
- Flutter performance best practices
- const class modifier (Dart docs)
- Keys in Flutter (API docs)
- Provider select documentation
- flutter_lints package
Frequently asked questions
When does const actually prevent rebuilds?
const tells Flutter the widget instance is canonical and immutable at compile time. The parent can reuse the same child instance across rebuilds, skipping rebuild work for that subtree. It does not stop rebuilds caused by InheritedWidget changes above the const widget.
Should every widget be const?
Only where all constructor arguments are compile-time constants. Use const on static UI chrome—padding, icons, text styles, child widgets with literal values. Dynamic data from API responses cannot be const.
When do I need a Key on list items?
When list order changes, items are inserted or removed mid-list, or stateful children must preserve state across parent rebuilds. Use ValueKey with stable business IDs, not array index, for reorderable or filtered lists.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →