Performance Optimization
Make your Flutter apps fast, smooth, and memory-efficient with proven techniques.
How Const Saves Performance
When you mark a widget as const, Flutter creates it once and reuses the same instance forever. Without const, Flutter creates a new object on every build — even if nothing changed.
const_impact.dart
// ❌ Bad: Creates a NEW Text widget on every rebuild
Text('Hello World')
// ✅ Good: Created ONCE, reused forever
const Text('Hello World')
// ❌ Bad: New Padding + new Text on every rebuild
Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello'),
)
// ✅ Good: Entire subtree is const
const Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)
💡 The Golden Rule
If a widget subtree doesn't depend on any runtime variables (state, theme, media query, etc.), make it
const. This is the single most impactful performance optimization you can apply to any Flutter app.
Avoiding Unnecessary Rebuilds
Every setState() call rebuilds the entire widget subtree under the State. Minimize rebuild scope by:
- Moving frequently-updating widgets into their own StatefulWidget
- Extracting constant subtrees into separate const widgets
- Using
constconstructors on all static parts of your UI - Profiling with Flutter DevTools to find expensive rebuilds
Const Constructors
Using const eliminates widget rebuilds by making them immutable at compile time:
const_examples.dart
// ❌ Bad — creates a new widget every build
Text('Hello', style: TextStyle(fontSize: 16))
// ✅ Good — constant, never rebuilt
const Text('Hello', style: TextStyle(fontSize: 16))
// ✅ Better — extract to a const static field
static const _titleStyle = TextStyle(fontSize: 16, fontWeight: FontWeight.bold);
Text('Hello', style: _titleStyle)
// ✅ Mark entire widget trees as const
const Padding(
padding: EdgeInsets.all(16),
child: Column(children: [
Text('Title'),
Text('Subtitle'),
]),
)
💡 Quick Win
Run
dart fix --apply to automatically add const where possible. The Flutter linter will also warn about missing const constructors.
Build Method Optimization
Keep build methods pure and avoid expensive operations:
optimized_build.dart
// ❌ Bad — expensive computation in build
@override
Widget build(BuildContext context) {
var filtered = items.where((i) => i.price > 10).toList();
return ListView.builder(itemCount: filtered.length, ...);
}
// ✅ Good — compute once, cache result
late List<Item> _filteredItems;
@override
void initState() {
super.initState();
_filteredItems = items.where((i) => i.price > 10).toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(itemCount: _filteredItems.length, ...);
}
// ✅ Better — use RepaintBoundary for lists
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary(
child: ListTile(title: Text(items[index].name)),
);
},
)
Avoiding Unnecessary Repaints
Use RepaintBoundary to isolate widget rebuilds and prevent cascading repaints:
repaint_boundary.dart
// Use ValueKey to help Flutter identify stable widgets
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary(
key: ValueKey(items[index].id),
child: ProductCard(product: items[index]),
);
},
)
// Avoid setState on parent when only child changes
// ❌ Bad: setState on entire screen
setState(() { _counter++; }); // Rebuilds everything
// ✅ Good: Extract counter to its own StatefulWidget
class CounterWidget extends StatefulWidget {
// Only this widget rebuilds
}
Memory Leak Prevention
Always dispose controllers, streams, and subscriptions:
dispose_example.dart
class MyScreenState extends State<MyScreen> {
late AnimationController _controller;
late StreamSubscription _subscription;
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_scrollController = ScrollController();
_subscription = eventStream.listen((e) => print(e));
}
@override
void dispose() {
_controller.dispose(); // ✅ Always dispose
_subscription.cancel(); // ✅ Always cancel
_scrollController.dispose(); // ✅ Always dispose
super.dispose();
}
}
⚠️ Common Memory Leaks
- Forgetting to dispose AnimationControllers
- Not cancelling StreamSubscriptions
- Keeping references to old widgets in callbacks
- Using timers without cancelling them
Profiling Tools
| Tool | Purpose | Command |
|---|---|---|
| DevTools | Performance, memory, layout inspector | flutter run --profile |
| Timeline | Frame rendering analysis | flutter run --profile --trace-systrace |
| Widget Inspector | Widget tree and rebuild analysis | DevTools → Inspector |
| Size Report | Code splitting analysis | flutter build apk --analyze-size |
Summary
- Use
constconstructors everywhere possible - Keep build methods simple — extract expensive work
- Use
RepaintBoundaryto isolate repaints - Always
dispose()controllers, streams, and subscriptions - Profile with DevTools to find real bottlenecks
🚀 Next Step Continue to Flutter Testing to learn about unit, widget, and integration tests.