You press a button.
You change one value.
Suddenly your whole screen rebuilds even widgets that had nothing to do with the change.
You might be thinking:
“But I only called
setState()
once… what’s the problem?”
Flutter is fast but unnecessary widget rebuilds can ruin performance, cause visual flicker, battery drain, and even crash low-end devices.
In this post, we’re going deep into:
- What actually causes rebuilds
- Real-world examples of excessive rebuilds
- Tools to detect them
- And how to fix them the right way
If you want your Flutter app to feel smooth, battery-friendly, and production-grade this is for you.
How Flutter Really Rebuilds Widgets
Flutter’s UI is declarative.
That means you don’t manually change UI elements you describe what the UI should look like at any given state.
When state changes, Flutter rebuilds affected widgets.
But here’s the catch:
Flutter rebuilds from the top of the widget tree down unless you break it into smaller, optimized parts.
And sometimes, it rebuilds more than you expected.
Common Rebuild Triggers
Here are the most common ways widgets get rebuilt (sometimes unnecessarily):
-
setState()
— Rebuilds the entire widget that called it -
InheritedWidget
(e.g.Theme
,MediaQuery
) — Rebuilds all widgets using it when it changes -
Provider
,Riverpod
,Bloc
— Depends on how you consume the state — poor use = unnecessary rebuilds - Parent widget rebuilds — All child widgets rebuild unless marked
const
or separated
Real-World Bug: App Feels Laggy on Every Tap
Let’s say you built a shopping cart screen:
class CartScreen extends StatefulWidget {
@override
_CartScreenState createState() => _CartScreenState();
}
class _CartScreenState extends State<CartScreen> {
int quantity = 1;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Cart'),
ElevatedButton(
onPressed: () => setState(() => quantity++),
child: Text('Add'),
),
ProductImage(), // This shouldn't change
Text('Quantity: $quantity'),
],
);
}
}
Problem:
When you press “Add”, setState
triggers a rebuild of the entire CartScreen
, including ProductImage()
which is expensive to rebuild.
Solution:
Extract ProductImage
into a StatelessWidget
, outside the rebuild scope:
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Cart'),
ElevatedButton(
onPressed: () => setState(() => quantity++),
child: Text('Add'),
),
const ProductImage(), // const prevents rebuild!
Text('Quantity: $quantity'),
],
);
}
How to Detect Rebuilds
Flutter gives you a few powerful ways to catch excessive rebuilding:
1. Use debugPrintRebuildDirtyWidgets = true;
Add this in main()
:
void main() {
debugPrintRebuildDirtyWidgets = true;
runApp(MyApp());
}
Now every widget rebuild will be printed in the debug console.
2. Wrap widgets in RepaintBoundary
RepaintBoundary(
child: SomeExpensiveWidget(),
)
This separates the render pipeline Flutter won’t repaint this widget unless its own state changes.
3. Use Flutter DevTools
Go to Flutter DevTools > Performance > Rebuild Stats
You’ll see exactly which widgets rebuilt and how often.
This is gold when debugging complex UI behavior.
Case Study: Flutter App Lagging on List Scroll
A developer built a chat app. Each message item had:
- Profile picture
- Name
- Last message
- Timestamp
- Online status
Whenever any new message arrived all messages were rebuilding. Why?
Problem:
They used a ListView.builder
, but placed the entire message list inside a widget that updated when any message changed.
BlocBuilder<ChatCubit, ChatState>(
builder: (context, state) {
return ListView.builder(
itemCount: state.messages.length,
itemBuilder: (context, index) {
return ChatTile(message: state.messages[index]);
},
);
},
)
Every time the ChatCubit
emitted a new message, the entire ListView
rebuilt.
Fix:
Use ListView.separated
or optimize rebuilds by checking which items actually changed, or use indexed rebuild logic (like flutter_bloc
’s buildWhen
).
Best Practices to Prevent Excessive Rebuilds
Do
- Extract widgets into small, reusable components
- Use
const
constructors when possible - Use
Selector
orConsumer
wisely in Provider - Use
buildWhen
/select
to control rebuild triggers - Use
RepaintBoundary
for images, charts, maps
Don’t
- Put all UI inside one giant
build()
- Forget to mark stateless widgets as
const
- Wrap entire widget trees in
BlocBuilder
- Allow every state change to rebuild the whole tree
- Let heavy widgets redraw every frame
Quick Tips
- Use
const
liberally especially for static widgets - Memorize data or cache widgets if they don’t change often
- Use
ValueNotifier
andValueListenableBuilder
for simple UI state (faster than full state management for small tasks) - Profile with DevTools every time you add new UI logic
Summary: What You Learned
- Flutter rebuilds widgets declaratively, but you control the scope
- Rebuilding too much = lag, Jank, bad UX
- Use tools like
debugPrintRebuildDirtyWidgets
,RepaintBoundary
, and DevTools - Optimize using
const
, smaller widgets, and smart state consumption
Final Thought
Your Flutter app may “work,” but that doesn’t mean it’s efficient.
Understanding how and when widgets rebuild helps you write smoother, cleaner, production-ready code.
So next time you face a lag or Jank that doesn’t make sense check who’s rebuilding. It might just be that innocent widget you forgot to const
.
Related Reads
[
- Understanding Dart’s Event Loop: Why Your Async Code Acts Weird](https://alaminkarno.medium.com/understanding-darts-event-loop-why-your-async-code-acts-weird-146748da0769)
- Stop Using
initState()
Like That: Async, Await & Flutter’s Lifecycle Explained
Top comments (0)