Flutter Performance Demystified

Flutter’s reactive architecture and robust widget system make it a go-to choice for developers. However, like any framework, achieving optimal performance in Flutter apps requires following best practices and fine-tuning your code.

As we all know performance is an important aspect and tuning it involves optimizing the performance of your Flutter app by minimizing its resource usage and avoiding common pitfalls.

1. Understanding Flutter’s Performance Bottlenecks

Before diving into optimization techniques, it’s essential to understand the two primary performance areas in Flutter:

UI Thread (Rendering Performance): Handles the rendering of widgets, animations, and layouts. Bottlenecks here can result in dropped frames and janky animations.

Dart Thread (Business Logic): Executes app logic, API calls, and computations. Heavy operations on this thread can cause delays in user interactions.

2. Widget Build Optimization

  • Avoid unnecessary widget rebuilds:

Flutter’s UI is built declaratively, which means widgets are rebuilt frequently. Unnecessary rebuilds can degrade performance.

  • Use key properties on widgets that change frequently to prevent unnecessary rebuilds of their entire subtrees.

  • Consider using KeepAlive to preserve the state of widgets that are scrolled off-screen but might reappear soon.

  • Use const constructors where possible:

By declaring widgets with const, you signal to Flutter that the widget is immutable, improving its build performance.

class StaticText extends StatelessWidget {
  const StaticText({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Text('This text doesn’t change');
  }
}
  • Minimize the use of StatefulWidgets:

Opt for StatelessWidgets whenever possible, as they are inherently more performant.

  • Use RepaintBoundary for widgets that don’t need to be repainted frequently.
RepaintBoundary(
  child: Image.asset('assets/static_image.png'),
)
  • Avoid Overloading the build() Method

Move logic out of the build() method to prevent performance issues.

Example of what to avoid:

@override
Widget build(BuildContext context) {
  final data = fetchData(); // Avoid fetching data here.
  return Text(data);
}

Fetch data outside the build() method and use state management to update the UI.

@override
Widget build(BuildContext context) {
  return FutureBuilder<String>(
    future: fetchData(),
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        return const CircularProgressIndicator();
      } else if (snapshot.hasError) {
        return const Text('Error');
      }
      return Text(snapshot.data ?? '');
    },
  );
}
  • Avoid Deeply Nested Widget Hierarchies:

excessively deep widget trees can lead to poor readability, maintainability, and performance degradation, as the rendering engine has to traverse a complex hierarchy. Try to keep your widget tree as shallow as possible. Instead of nesting widgets, you can use the ListView or GridView widgets to display lists and grids of items.

Example: To avoid

Widget build(BuildContext context) {
  return Container(
    child: Column(
      children: [
        Container(
          child: Row(
            children: [
              Icon(Icons.star),
              Text('Deeply Nested Widget'),
            ],
          ),
        ),
      ],
    ),
  );
}

Example: Better

Widget build(BuildContext context) {
  return Container(
    child: Column(
      children: [
        StarRow(),
      ],
    ),
  );
}

class StarRow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(Icons.star),
        Text('Simplified Widget'),
      ],
    );
  }
}

3. Efficient State Management

Efficiently managing the state is critical for maintaining a responsive and performant app.There are various state management techniques which Flutter offers we can choose amongst them as per our use case and benefits it provides.For instance, use Provider for simpler apps and Bloc for more complex state management needs.

Example: Managing a counter app state with Provider.

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

ChangeNotifierProvider(
  create: (context) => Counter(),
  child: MyApp(),
)

Access state in widgets:

Consumer<Counter>(
  builder: (context, counter, child) {
    return Text('Count: ${counter.count}');
  },
)

Wrap parts of the UI in Consumer or Selector widgets to limit rebuilds.

4. Asynchronous Programming and Background Tasks

  • Avoid Blocking the UI Thread

Use compute() for expensive computations to offload work to a separate thread off the main thread to prevent UI freezes.

int heavyComputation(int input) {
  // Simulate a time-consuming task.
  return input * 2;
}

Future<int> performComputation(int input) async {
  return await compute(heavyComputation, input);
}
  • Optimize Network Requests

Use efficient API clients like Dio and cache results to minimize redundant calls. Implementing caching mechanisms can reduce the number of network requests and improve performance.

Example: Using dio for API requests and saving as cache data

final dio = Dio();

Future<void> fetchData() async {
  final response = await dio.get('https://api.example.com/data');
  final _cache = <String, dynamic>{};

  void cacheData(String key, dynamic data) {
    _cache[key] = data;
  }

  dynamic getData(String key) {
    return _cache[key];
  }
  if (response.statusCode == 200) {
    // Process data.
  }
}

4. Image Optimization

  • Use cached_network_image :

Avoid loading images repeatedly by caching them, reducing load times and memory usage.

CachedNetworkImage(
  imageUrl: 'https://exampleImage.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
);
  • Resize Large Images:

Resize images to reduce memory usage using the Image widget’s properties.

Image.asset(
  'assets/large_image.png',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
)
  • Preload Images:

Preload images for faster display using precacheImage().

@override
void initState() {
  super.initState();
  precacheImage(AssetImage('assets/image.png'), context);
}

5. Efficiently use Lists and Grids

  • Use ListView.builder for Large Lists:

Avoid using ListView with many child widgets as it loads all widgets at once unlike creating them on the fly only when they are visible, thus improving performance.

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(title: Text(items[index]));
  },
)
  • Paginate Large Data Sets:

Load large datasets as needed in chunks using pagination, reducing initial load times and memory usage.

PagedListView<int, Item>(
  pagingController: _pagingController,
  builderDelegate: PagedChildBuilderDelegate<Item>(
    itemBuilder: (context, item, index) => ListTile(title: Text(item.name)),
  ),
)

6. Profiling and Debugging Performance

  • Use Flutter DevTools:

Leverage Flutter’s built-in profiling tools to identify bottlenecks. Analyze widget rebuilds.

  • Analyze widget rebuilds.

  • Profile memory usage.

  • Measure frame rendering times.

flutter pub global activate devtools
flutter run --profile
  • Diagnose Rebuilds:

Wrap widgets with WidgetInspector to detect excessive rebuilds.

  • Optimize Skipped Frames:

Address skipped frames by analyzing the timeline in DevTools and optimizing bottlenecks.

Conclusion

Optimizing Flutter performance is about striking a balance between writing clean, maintainable code and leveraging best practices to enhance app efficiency. Improving performance in Flutter apps requires a combination of best practices, efficient coding, and leveraging profiling tools.

By following the strategies outlined in this guide, developers can ensure their Flutter applications not only look stunning but also perform seamlessly, delighting users across platforms.

0
Subscribe to my newsletter

Read articles from NonStop io Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

NonStop io Technologies
NonStop io Technologies

Product Development as an Expertise Since 2015 Founded in August 2015, we are a USA-based Bespoke Engineering Studio providing Product Development as an Expertise. With 80+ satisfied clients worldwide, we serve startups and enterprises across San Francisco, Seattle, New York, London, Pune, Bangalore, Tokyo and other prominent technology hubs.