Home
Dart
OOP
Flutter
Contact

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:

  1. Moving frequently-updating widgets into their own StatefulWidget
  2. Extracting constant subtrees into separate const widgets
  3. Using const constructors on all static parts of your UI
  4. 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

ToolPurposeCommand
DevToolsPerformance, memory, layout inspectorflutter run --profile
TimelineFrame rendering analysisflutter run --profile --trace-systrace
Widget InspectorWidget tree and rebuild analysisDevTools → Inspector
Size ReportCode splitting analysisflutter build apk --analyze-size

Summary

  • Use const constructors everywhere possible
  • Keep build methods simple — extract expensive work
  • Use RepaintBoundary to 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.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← API Integration Next: Testing →