Flutter Performance Tips

Optimize your Flutter apps for speed and efficiency

⚡ Why Performance Matters?

Performance optimization ensures your Flutter app runs smoothly with fast load times, responsive UI, and efficient resource usage. Good performance leads to better user experience and higher app ratings.


// Optimized widget with const
const Text('Fast Performance');

// Use ListView.builder for efficiency
ListView.builder(itemBuilder: (context, index) => Item());
                                    

Performance Optimization Areas

🎨

Widget Optimization

Reduce unnecessary widget rebuilds using const constructors, keys, and proper widget structure to minimize rendering overhead and improve frame rates.

Const Keys
📋

List Performance

Use lazy loading with ListView.builder and GridView.builder to render only visible items, reducing memory usage and improving scroll performance.

Builder Lazy Load
🖼️

Image Optimization

Optimize images by caching, resizing, and using appropriate formats to reduce memory consumption and improve loading times across your app.

Caching Compression
⚙️

Build Optimization

Minimize expensive operations in build methods, use compute for heavy tasks, and profile your app to identify performance bottlenecks.

Profiling Compute

🔹 Widget Performance Tips

🔸 Use Const Constructors

Const widgets are created once and reused, preventing unnecessary rebuilds. This is one of the easiest and most effective optimizations.

// Good: Const widgets don't rebuild
const Text('Hello');
const Icon(Icons.home);
const Padding(
  padding: EdgeInsets.all(8.0),
  child: Text('Optimized'),
)

// Avoid: Non-const rebuilds every time
Text('Hello');
Icon(Icons.home);

🔸 Minimize Widget Rebuilds

// Bad: Entire widget rebuilds
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  int counter = 0;
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ExpensiveWidget(), // Rebuilds unnecessarily
        Text('$counter'),
      ],
    );
  }
}

// Good: Only counter rebuilds
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  int counter = 0;
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveWidget(), // Doesn't rebuild
        Text('$counter'),
      ],
    );
  }
}

🔸 Use Keys Wisely

// Use keys for list items that can reorder
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      key: ValueKey(items[index].id),
      title: Text(items[index].name),
    );
  },
)

🔹 List Performance Optimization

🔸 Use ListView.builder

ListView.builder creates items on-demand as they scroll into view, using much less memory than creating all items at once.

// Good: Lazy loading
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

// Avoid: Creates all 1000 items immediately
ListView(
  children: List.generate(
    1000,
    (index) => ListTile(title: Text('Item $index')),
  ),
)

🔸 Use GridView.builder

// Efficient grid with lazy loading
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
  ),
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Card(child: Text(items[index]));
  },
)

🔸 Add Separators Efficiently

// Use ListView.separated for lists with dividers
ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
  separatorBuilder: (context, index) => const Divider(),
)

🔹 Image Performance Tips

🔸 Cache Network Images

// Use cached_network_image package
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
  // Images are cached automatically
)

🔸 Specify Image Dimensions

Always specify width and height to prevent unnecessary resizing and improve rendering performance.

// Good: Specified dimensions
Image.network(
  'url',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
)

// Avoid: No dimensions specified
Image.network('url')

🔸 Use Appropriate Image Formats

  • WebP: Best compression, smaller file sizes
  • PNG: For images with transparency
  • JPEG: For photos without transparency
  • SVG: For icons and simple graphics

🔸 Optimize Image Assets

// Provide multiple resolutions
Image.asset(
  'assets/images/logo.png',
  // Flutter automatically picks:
  // logo.png (1x)
  // 2.0x/logo.png (2x)
  // 3.0x/logo.png (3x)
)

🔹 Build Method Optimization

🔸 Avoid Heavy Operations in Build

// Bad: Expensive operation in build
@override
Widget build(BuildContext context) {
  final data = expensiveCalculation(); // Runs every rebuild
  return Text(data);
}

// Good: Calculate once in initState
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  late String data;
  
  @override
  void initState() {
    super.initState();
    data = expensiveCalculation(); // Runs once
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(data);
  }
}

🔸 Use Compute for Heavy Tasks

Use compute() to run expensive operations in a separate isolate, preventing UI freezing.

import 'package:flutter/foundation.dart';

// Heavy computation function
List processData(List data) {
  // Expensive operation
  return data.map((e) => e * 2).toList();
}

// Use compute to run in background
Future loadData() async {
  final result = await compute(processData, largeDataList);
  setState(() {
    processedData = result;
  });
}

🔸 Avoid Creating Objects in Build

// Bad: Creates new TextStyle every build
@override
Widget build(BuildContext context) {
  return Text(
    'Hello',
    style: TextStyle(fontSize: 20, color: Colors.blue),
  );
}

// Good: Reuse const TextStyle
static const textStyle = TextStyle(fontSize: 20, color: Colors.blue);

@override
Widget build(BuildContext context) {
  return Text('Hello', style: textStyle);
}

🔹 State Management Performance

🔸 Use Provider Efficiently

// Good: Only rebuild when needed
Consumer(
  builder: (context, counter, child) {
    return Text('${counter.count}');
  },
)

// Avoid: Rebuilds entire widget tree
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of(context);
    return Column(
      children: [
        ExpensiveWidget(), // Rebuilds unnecessarily
        Text('${counter.count}'),
      ],
    );
  }
}

🔸 Use Selector for Specific Values

// Only rebuilds when count changes
Selector(
  selector: (context, provider) => provider.count,
  builder: (context, count, child) {
    return Text('Count: $count');
  },
)

🔹 Animation Performance

🔸 Use AnimatedBuilder

// Efficient animation
class MyAnimation extends StatefulWidget {
  @override
  _MyAnimationState createState() => _MyAnimationState();
}

class _MyAnimationState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _controller.value * 2 * 3.14,
          child: child,
        );
      },
      child: const Icon(Icons.star), // Doesn't rebuild
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

🔸 Use Implicit Animations

// Simple, performant animations
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  width: isExpanded ? 200 : 100,
  height: isExpanded ? 200 : 100,
  color: isExpanded ? Colors.blue : Colors.red,
)

AnimatedOpacity(
  opacity: isVisible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 500),
  child: Text('Fade in/out'),
)

🔹 Memory Management

🔸 Dispose Controllers

Always dispose controllers, streams, and listeners to prevent memory leaks.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  late TextEditingController _controller;
  late ScrollController _scrollController;
  
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
    _scrollController = ScrollController();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    _scrollController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return TextField(controller: _controller);
  }
}

🔸 Cancel Streams and Timers

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  StreamSubscription? _subscription;
  Timer? _timer;
  
  @override
  void initState() {
    super.initState();
    _subscription = stream.listen((data) {
      // Handle data
    });
    
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      // Periodic task
    });
  }
  
  @override
  void dispose() {
    _subscription?.cancel();
    _timer?.cancel();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

🔹 Profiling and Debugging

🔸 Use Flutter DevTools

Flutter DevTools provides performance profiling, memory analysis, and widget inspection tools.

# Open DevTools
flutter pub global activate devtools
flutter pub global run devtools

# Or run with your app
flutter run --profile

🔸 Check Performance Overlay

// Enable performance overlay
MaterialApp(
  showPerformanceOverlay: true,
  home: MyHomePage(),
)

// Shows FPS and frame rendering times

🔸 Profile Your App

# Build in profile mode
flutter run --profile

# Build in release mode for final testing
flutter run --release

Important: Never test performance in debug mode. Always use profile or release mode for accurate results.

🔹 Quick Performance Checklist

✅ Before Release:

  • Use const constructors wherever possible
  • Use ListView.builder for long lists
  • Cache and optimize images
  • Dispose all controllers and streams
  • Avoid expensive operations in build()
  • Profile app in release mode
  • Test on real devices, not just emulators
  • Check memory usage with DevTools
  • Minimize widget rebuilds
  • Use compute() for heavy computations

🧠 Test Your Knowledge

Which is better for a list of 1000 items?