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 constor 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 constconstructors when possible
- Use SelectororConsumerwisely in Provider
- Use buildWhen/selectto control rebuild triggers
- Use RepaintBoundaryfor 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 constliberally especially for static widgets
- Memorize data or cache widgets if they don’t change often
- Use ValueNotifierandValueListenableBuilderfor 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)