How to improve your performance in Flutter: Tips and best practices 

4 min read
zen8labs How to improve your performance in Flutter: Tips and best practices

Flutter has transformed the landscape of cross-platform app development, enabling developers to craft beautiful, fast, and responsive applications for mobile, web, and desktop from a single codebase. However, to ensure a seamless user experience, performance optimization is paramount. In my blog, I will expand on my previous blog and provide you with some steps that can turn these into actionable strategies and best practices to enhance performance in Flutter applications, covering key areas like rendering, state management, and more.

1. Rendering performance

Flutter’s rendering engine is renowned for delivering smooth animations and transitions. However, unoptimized rendering can lead to janky frames and a subpar user experience. One powerful tool to improve rendering performance is the Repaint Boundary widget.

How it works

The RepaintBoundary widget isolates parts of the widget tree, limiting the scope of repaints. When a change occurs, only the widgets within the boundary are repainted, sparing static elements from unnecessary redraws.

Example

RepaintBoundary( 
  child: Container( 
    decoration: BoxDecoration( 
      image: DecorationImage( 
        image: AssetImage('assets/background.jpg'), 
        fit: BoxFit.cover, 
      ), 
    ), 
  ), 
) 

In this case, a static background image wrapped in RepaintBoundary won’t repaint when dynamic content above it changes, boosting performance.

2. Efficient widget construction 

Widgets are the foundation of Flutter’s UI, and optimizing their construction can yield significant performance gains. Enter const constructors.

Why it matters

Widgets created with const are instantiated at compile-time rather than runtime, reducing overhead during app execution. This is ideal for static widgets like icons, text, or layout elements that don’t change.

Implementation

const Text( 
  'Welcome to Flutter!', 
  style: TextStyle(fontSize: 20), 
) 

By using const, Flutter avoids rebuilding this widget unnecessarily, saving resources.

3. State management

As apps grow in complexity, managing state efficiently becomes critical to avoid performance pitfalls like excessive rebuilds. Solutions such as the Provider package can streamline state management.

Benefits

The Provider allows you to manage a higher state in the widget tree and selectively rebuild only the widgets that depend on that state, minimizing redundant updates.

Example

class CounterModel with ChangeNotifier { 
  int _count = 0; 
  int get count => _count; 
  void increment() { 
    _count++; 
    notifyListeners(); 
  } 
} 
 
Consumer<CounterModel>( 
  builder: (context, counter, child) => Text('Count: ${counter.count}'), 
)

Here, only the Text widget rebuilds when the counter changes, not the entire tree.

4. Avoiding unnecessary rebuilds

The build method in Flutter widgets runs whenever a widget updates. Frequent or heavy rebuilds can degrade performance. Techniques like memoization and caching can help.

How to optimize

Memoization caches the results of expensive computations, reusing them when inputs remain unchanged. You can implement this manually or use packages like memoize.

Example

int expensiveCalculation(int input) { 
  // Simulate heavy work 
  return input * 2; 
} 
 
final memoizedCalc = memoize(expensiveCalculation); 
 
Widget build(BuildContext context) { 
  return Text('Result: ${memoizedCalc(5)}'); 
}

This ensures the calculation isn’t repeated unless the input changes.

5. Optimizing lists and grids

Large lists or grids can strain performance if all items are built at once. Flutter’s ListView.builder and GridView.builder offer a solution.

Why they are better?

These widgets use lazy loading, constructing only the items currently visible on the screen, which reduces memory usage and enhances scrolling performance.

Example

ListView.builder( 
  itemCount: 1000, 
  itemBuilder: (context, index) => ListTile( 
    title: Text('Item $index'), 
  ), 
)

Only the visible items are rendered, making it ideal for long lists.

6. Image optimization

Images can slow down your app if not handled properly. Optimizing image loading and display is key to maintaining performance.

Tips

  • Use the cached_network_image package to cache images fetched from the web, reducing load times. 
  • Resize images to match their display size using the width and height properties.

Example

CachedNetworkImage( 
  imageUrl: 'https://example.com/image.jpg', 
  width: 100, 
  height: 100, 
  placeholder: (context, url) => CircularProgressIndicator(), 
)

This caches the image and scales it appropriately, saving memory and time.

7. Improving app startup time

A slow startup can frustrate users. Techniques like code splitting, lazy loading, and the compute function can speed things up.

Implementation

The compute function offloads heavy tasks to a separate isolate, keeping the main thread responsive.

Example

Future<int> heavyTask(List<int> data) async { 
  return data.reduce((a, b) => a + b); 
} 
 
void loadData() async { 
  final result = await compute(heavyTask, [1, 2, 3, 4, 5]); 
  print('Result: $result'); 
} 
 

This keeps the UI smooth during startup. 

8. Efficient Animations

Animations enhance UX but can tax performance if overdone. Use AnimationController and Tween for efficiency.

Example

class MyWidget extends StatefulWidget { 
  @override 
  _MyWidgetState createState() => _MyWidgetState(); 
} 
 
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin { 
  late AnimationController _controller; 
  late Animation<double> _animation; 
 
  @override 
  void initState() { 
    super.initState(); 
    _controller = AnimationController( 
      duration: Duration(seconds: 1), 
      vsync: this, 
    ); 
    _animation = Tween<double>(begin: 0, end: 1).animate(_controller); 
    _controller.forward(); 
  } 
 
  @override 
  Widget build(BuildContext context) { 
    return FadeTransition( 
      opacity: _animation, 
      child: Text('Hello!'), 
    ); 
  } 
} 

This creates a smooth fade-in effect without performance overhead.

9. Best Practices and Tips

Here are a few additional tips to ensure your Flutter app runs at peak performance: 

  • Minimize Overdraw: Reduce the number of layers in your widget tree. Use tools like the performance overlay to monitor overdraw. 
  • Reduce Build Complexity: Split large widgets into smaller ones to limit the scope of rebuilds. 
  • Test on Real Devices: Emulators might not accurately represent performance on actual devices. Regularly test on low-end hardware to ensure smooth performance across the board. 
  • Keep Dependencies Updated: Flutter and its plugins are frequently updated with performance improvements. Keeping your dependencies current can yield automatic benefits.

10. Testing and Profiling

Identifying bottlenecks requires testing and profiling. Flutter’s DevTools suite is perfect for this. 

How to Use 

  • Open DevTools via flutter run –profile. 
  • Use the “Performance” tab to analyze frame times and spot jank. 
  • Check the “Memory” tab for leaks or excessive allocations. 

Regular profiling ensures your optimizations are effective.

Conclusion

Optimizing performance in Flutter involves a holistic approach—tackling rendering, widget efficiency, state management, and beyond, taking the time to make sure you deal with the frequent updates can help to improve performance enhancements and fixes. However by applying these tips that I have given you such as using RepaintBoundary, leveraging const constructors, managing state wisely, and profiling with DevTools—you can build apps that are fast, fluid, and user-friendly. Start implementing these best practices today and watch your Flutter apps shine. I know Flutter is an interesting platform, but there are many others you can learn about from my colleagues where we share insights on all things IT, to allow everyone to create something awesome!

Thanh Nguygen, Mobile Engineer

Related posts

What is an OkHttp Interceptor? They are a powerful tools that allow for calls to flow efficiently. Our new blog gives quick insights into all things interceptor.
3 min read
What is a demo in Swift package? Find out the ways that having a demo in your Swift package can help make your project easier to maintain, update and reuse.
5 min read
Property wrappers in Swift provide a powerful and reusable way to encapsulate property logic. The compiler plays a critical role in synthesizing the necessary code for the backing store, initializers, and projected values. By understanding how the Swift compiler handles property wrappers behind the scenes, you can make better use of these features in your projects.
6 min read