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.
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.