Pushing Flutter to the Limit: Performance Tips Every Developer Should Know

Anuj Kumar Pandey
16 min readSep 1, 2023

--

Ever felt like Flutter was the tortoise in the race? Fret not! With a few tricks up our sleeves, we can turn that tortoise into a turbo-charged hare. Ready to zoom? Let’s dive into some Flutter performance tips!

Flutter applications are performant by default, so you only need to avoid common pitfalls to get excellent performance. How you design and implement your app’s UI can have a big impact on how efficiently it runs.

These best practice recommendations will help you write the most performant Flutter app possible.

So lets start reading it!!

1. Use Clean Architecture

Clean Architecture is a software design pattern that emphasizes separation of concerns and independent testing. This pattern encourages the separation of application logic into different layers, with each layer responsible for a specific set of tasks. Clean Architecture can be a great fit for large-scale apps because it provides a clear separation of concerns and enables easier testing.

You can check this package — clean_architecture_scaffold

Here’s an example of a Clean Architecture implementation in Flutter:

lib/
data/
models/
user_model.dart
repositories/
user_repository.dart
domain/
entities/
user.dart
repositories/
user_repository_interface.dart
usecases/
get_users.dart
presentation/
pages/
users_page.dart
widgets/
user_item.dart
main.dart

2. Use Good State Management

State management plays a crucial role in Flutter app performance. Choose the right state management approach based on the complexity of your app. For small to medium-sized apps, the built-in setState method may be sufficient. However, for larger and more complex apps, consider using state management libraries like bloc or riverpod.

// Bad Approach
setState(() {
// Updating a large data structure unnecessarily
myList.add(newItem);
});

// Better Approach
final myListBloc = BlocProvider.of<MyListBloc>(context);
myListBloc.add(newItem);

3. Use Code Analysis Tools for Code Quality

Code Analysis tools, such as the Flutter Analyzer and Lint, can be incredibly helpful for improving code quality and reducing the risk of bugs and errors. These tools can help to identify potential issues before they become a problem and can also provide suggestions for improving code structure and readability.

Here’s an example of using the Flutter Analyzer in Flutter:

flutter analyze lib/

4. Use Automated Testing for Code Reliability

Automated Testing is an essential part of building large-scale apps because it helps to ensure that the code is reliable and performs as expected. Flutter provides several options for automated testing, including unit tests, widget tests, and integration tests.

Here’s an example of using the Flutter Test package for automated testing:

void main() {
test('UserRepository returns a list of users', () {
final userRepository = UserRepository();
final result = userRepository.getUsers();
expect(result, isInstanceOf<List<User>>());
});
}

5. Use Flutter Inspector for Debugging

Flutter Inspector is a powerful tool for debugging Flutter apps. It allows developers to inspect and manipulate the widget tree, view performance metrics, and more. Flutter Inspector can be accessed through the Flutter DevTools browser extension or through the command line.

Here’s an example of using Flutter Inspector for debugging:

flutter run --debug

6. Lazy Loading and Pagination

Fetching and rendering large amounts of data at once can significantly impact performance. Implement lazy loading and pagination to load data as needed, especially for long lists or data-intensive views.

// Bad Approach
// Fetch and load all items at once.
List<Item> allItems = fetchAllItems();

// Better Approach
// Implement lazy loading and pagination.
List<Item> loadItems(int pageNumber) {
// Fetch and return data for the specific page number.
}

// Use a ListView builder with lazy loading.
ListView.builder(
itemCount: totalPages,
itemBuilder: (context, index) {
return FutureBuilder(
future: loadItems(index),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// Build your list item here.
} else {
return CircularProgressIndicator();
}
},
);
},
);

7. Reduce Image Sizes

Large image files can slow down your app’s performance, especially when loading multiple images. Compress and resize images to reduce their file size without compromising too much on quality.

Example: Suppose you have an image with high resolution, but you only need it to be displayed in a smaller container in your app. Instead of using the original high-resolution image, you can resize it using the flutter_image_compress library.

import 'package:flutter_image_compress/flutter_image_compress.dart';

// Original image file
var imageFile = File('path/to/original/image.png');
// Get the image data
var imageBytes = await imageFile.readAsBytes();
// Resize and compress the image
var compressedBytes = await FlutterImageCompress.compressWithList(
imageBytes,
minHeight: 200,
minWidth: 200,
quality: 85,
);
// Save the compressed image to a new file
var compressedImageFile = File('path/to/compressed/image.png');
await compressedImageFile.writeAsBytes(compressedBytes);

8. Optimize Animations

Avoid using heavy or complex animations that can impact the app’s performance, especially on older devices. Use animations judiciously and consider using Flutter’s built-in animations like AnimatedContainer, AnimatedOpacity, etc.

// Bad Approach
// Using an expensive animation
AnimatedContainer(
duration: Duration(seconds: 1),
height: _isExpanded ? 300 : 1000,
color: Colors.blue,
);
// Better Approach
// Using a simple and efficient animation
AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isExpanded ? 300 : 100,
color: Colors.blue,
);

9. Optimize App Startup Time

Reduce the app’s startup time by optimizing the initialization process. Use the flutter_native_splash package to display a splash screen while the app loads, and delay the initialisation of non-essential components until after the app has started.

10. Avoid deep trees instead create a separate widget

You don’t want to keep on scrolling your IDE with a thousand lines of codes. Try creating a separate widget instead. It will look clean and easy to refactor.

//Bad
Column(
children: [
Container(
//some lengthy code here
),
Container(
//some another lengthy code
),
//some another lengthy code
],
)

//Good
Column(
children: [
FirstLengthyCodeWidget(),
SecondLengthyCodeWidget(),
//another lengthy code widget etc
],
)

11. Use cascade (..)

If you are just starting with flutter, You might have not used this operator but it is very useful when you want to perform some task on the same object.

//Bad
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

//Good
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;

12. Use spread operator (…)

This is another beautiful operator that dart has to offer. You can simply use this operator to perform many tasks such as if-else, join the list, etc.

//Bad
@override
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
isTrue ? const Text('One') : Container(),
isTrue ? const Text('Two') : Container(),
isTrue ? const Text('Three') : Container(),
],
),
);
}

//Good
@override
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
if(isTrue)...[
const Text('One'),
const Text('Two'),
const Text('Three')
]
],
),
);
}

13. Avoid using hardcoded style, decoration, etc.

If you are using a hardcoded style, decoration, etc in your application and later on if you decided to change those styles. You will be fixing them one by one.

//Bad
Column(
children: const [
Text(
'One',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
Text(
'Two',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
)

//Good
Column(
children: [
Text(
'One',
style: Theme.of(context).textTheme.subtitle1,
),
Text(
'Two',
style: Theme.of(context).textTheme.subtitle1,
),
],
),

14. Use build() with care

Avoid overly large single widgets with a large build() function. Split them into different widgets based on encapsulation but also on how they change.

As when setState() is called on a State object, all descendent widgets rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree.

Let’s see this example, we want that when the user presses the icon, only the colour of this icon changes.

So if we have all this UI in a single widget, when the icon is pressed, it will update the whole UI. What we can do is separate the icon into a StatefulWidget.

Before

import 'package:flutter/material.dart';

class FidgetWidget extends StatefulWidget {
const FidgetWidget({Key? key}) : super(key: key);

@override
_FidgetWidgetState createState() => _FidgetWidgetState();
}

class _FidgetWidgetState extends State<FidgetWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App Title'),
),
body: Column(
children: [
Text('Some Text'),
IconButton(
onPressed: () => setState(() {
// Some state change here
}),
icon: Icon(Icons.favorite),
),
],
),
);
}
}

After

import 'package:flutter/material.dart';

class MyIconWidget extends StatefulWidget {
const MyIconWidget({Key? key}) : super(key: key);

@override
_MyIconWidgetState createState() => _MyIconWidgetState();
}

class _MyIconWidgetState extends State<MyIconWidget> {
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => setState(() {

}),
icon: Icon(Icons.favorite),
);
}
}

15. Use Widgets over Functions

You can save CPU cycles and use with const constructor to make rebuild when only needed and much more benefits (reuse etc.. .)

//Bad
@override
Widget build(BuildContext context) {
return Column(
children: [
_getHeader(),
_getSubHeader(),
_getContent()
]
);
}

//Good
@override
Widget build(BuildContext context) {
return Column(
children: [
HeaderWidget(),
SubHeaderWidget(),
ContentWidget()
]
);
}

As Remi Rousselet, the creator of Riverpod, Provider and other packages says. “Classes have a better default behavior. The only benefit of methods is having to write a tiny bit less code. There’s no functional benefit.

16. Use final where possible

Using the final keyword can greatly improve the performance of your app. When a value is declared as final it can only be set once and does not change thereafter. This means that the framework does not need to constantly check for changes, leading to improved performance.

final items = ["Item 1", "Item 2", "Item 3"];

In this example, the variable items is declared as final, which means its value cannot be changed. This improves performance because the framework does not need to check for changes to this variable.

17. Use const where possible

x = Container(); 
y = Container();
x == y // false

x = const Container();
y = const Container();
x == y // true

If it’s already defined you can save RAM using the same widget. const widgets are created at compile time and hence are faster at runtime.

18. Use const constructors whenever possible

class CustomWidget extends StatelessWidget { 
const CustomWidget();

@override
Widget build(BuildContext context) {
...
}
}

When building your own widgets, or using Flutter widgets. This helps Flutter to rebuild only widgets that should be updated.

19. Use private variable/method whenever possible

Unless required, use a private keyword whenever possible.

//Bad
class Student {
String name;
String address;
Student({
required this.name,
required this.address,
});
}
}

//Good
class Student{
String _name;
String _address;
Student({
required String name,
required String address,
}):
_name = name,
_address = address;
}
Enough !!

Yes, its more like a dart best practice than performance. But, best practices can improve performance in someway, such as understanding code, reducing the complexity, etc.

20. Use nil instead const Container()

// good
text != null ? Text(text) : const Container()
// Better
text != null ? Text(text) : const SizedBox()
// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)

It’s just a basic Element Widget that does and costs almost nothing. See the package — nil.

21. Use itemExtent in ListView for long Lists

That helps Flutter to calculate ListView scroll position instead of calculating the height of every widget and make scroll animation much more performant.

By default, every child has to determine its extent which is quite expensive in terms of performance. Setting the value explicitly saves lots of CPU cycles. The longer the list is, the more speed you can gain with this property.

//Nope
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _listItems.length,
itemBuilder: (context, index) {
var item = _listItems[index];
return Center(
child: Text(item.toString())
);
}
}

//Good
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 150,
itemCount: _listItems.length,
itemBuilder: (context, index) {
var item = _listItems[index];
return Center(
child: Text(item.toString())
);
}
}

22. Avoid using AnimationController with setState

It causes to rebuild the whole UI not only animated widget and make animation laggy.

void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..addListener(() => setState(() {}));
}

Column(
children: [
Placeholder(), // rebuilds
Placeholder(), // rebuilds
Placeholder(), // rebuilds
Transform.translate( // rebuilds
offset: Offset(100 * _controller.value, 0),
child: Placeholder(),
),
],
),

To

void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
// No addListener(...)
}

AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return Transform.translate(
offset: Offset(100 * _controller.value, 0),
child: child,
);
},
child: Placeholder(),
),

23. Accelerate Flutter performance with Keys

Flutter recognizes Widgets better when using keys. This gives us better performance upto 4X.

// FROM 
return value ? const SizedBox() : const Placeholder(),
// TO
return value ? const SizedBox(key: ValueKey('SizedBox')) : const Placeholder(key: ValueKey('Placeholder')),
----------------------------------------------
// FROM
final inner = SizedBox();
return value ? SizedBox(child: inner) : inner,
// TO
final global = GlobalKey();
final inner = SizedBox(key: global);
return value ? SizedBox(child: inner) : inner,

Caution

ValueKey can make your code look a bit bloat

GlobalKey is a bit dangerous but sometimes it’s worth it.

24. Optimise memory when using image ListView

ListView.builder( 
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);

ListView couldn’t kill its the children are not being visible to the screen. It causes consume a lot of memory if children have high-resolution images.
By doing these options false, could lead to use of more GPU and CPU work, but it could solve our memory issue and you will get a very performant view without noticeable issues.

25. Use for/while instead of foreach/map

If you are going to deal with a huge amount of data, using the right loop might have an impact of your performance.

Source — https://itnext.io/comparing-darts-loops-which-is-the-fastest-731a03ad42a2

26. Pre-cache your images and icons

It depends on the scenario but I generally precache all images in the main.

For Images
You don’t need any packages, just use —

precacheImage(
AssetImage(imagePath),
context
);

For SVGs
You need flutter_svg package

precachePicture( 
ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath),
context
);

27. Use SKSL Warmup

flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache

If an app has janky animations during the first run, and later becomes smooth for the same animation, then it’s very likely due to shader compilation jank.

28. Consider using RepaintBoundary

This widget creates a separate display list for its child, which can improve performance in specific cases.

29. Use builder named constructors if possible

Listview.builder()

builder only renders displayed items on the screen. if you don’t use builder
renders all the children even if cannot be seen.

30. Don’t Use ShrinkWrap any scrollable widget

Measuring content problems.

31. USE ISOLATE when using heavy function

Some methods are pretty expensive such as image processing and they can freeze your app while working in the main thread. If you don’t want that kinda situation you should consider using isolates.

32. DO NOT USE ISOLATES for every little thing

Isolates are great, they are pretty helpful when you have done a heavy task. but if you use everywhere even the smallest operations your app can be very janky. Just because spawning an isolate isn’t that cheap operation to accomplish. It takes time and resources.

33. Proper disposal of data

Unnecessary ram usage kills inside the app silently. So don’t forget to dispose your data.

34. Compress your data for the sake of memory

final response = await rootBundle.loadString('assets/en_us.json');

final original = utf8.encode(response);

final compressed = gzip.encode(original);
final decompress = gzip.decode(compressed);

final enUS = utf8.decode(decompress);

You can also save some memory with this way.

35. Keep up to date Flutter

In every version Flutter getting faster and faster. So don’t forget to keep up to date with your flutter version and keep amazing works!!

36. Test Performance on Real Devices

Always test your app’s performance on real devices, including older models, to identify any performance issues that may not be apparent on emulators or newer devices.

37. Prefer StatelessWidget over StatefulWidget

A StatelessWidget is faster than a StatefulWidget because it doesn’t need to manage state as the name implies. That’s why you should prefer it if possible.

Choose a StatefulWidget when…

you need a preparation function with initState()

you need to dispose of resources with dispose()

you need to trigger a widget rebuild with setState()

your widget has changing variables (non-final)

In all other situations, you should prefer a StatelessWidget .

38. Don’t use OpacityWidget

The Opacity widget can cause performance issues when used with animations because all child widgets of the Opacity widget will also be rebuilt in every new frame. It is better to use AnimatedOpacity in this case. If you want to fade in an image, use the FadeInImage widget. If you want to have a color with opacity, draw a color with opacity.

//Bad
Opacity(opacity: 0.5, child: Container(color: Colors.red))
//Good
Container(color: Color.fromRGBO(255, 0, 0, 0.5))

39. Prefer SizedBox over Container

A Container widget is very flexible. You can for example customize the padding or the borders without nesting it in another widget. But if you only need a box with a certain height and width, it is better to use a SizedBox widget. It can be made const while a Container can’t.

To add whitespace in a Row/Column, prefer using a SizedBox to a Container.

//NOPE
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(header),
Container(height: 10),
Text(subheader),
Text(content)
]
);
}

//YES~
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(header),
const SizedBox(height: 10),
Text(subheader),
Text(content)
]
);
}

40. Don’t use clipping

Clipping is a very expensive operation and should be avoided when your app gets slow. It gets even more expensive if the clipping behavior is set to Clip.antiAliasWithSaveLayer. Try to find other ways to achieve your goals without clipping. For example, a rectangle with rounded borders can be done with the borderRadius property instead of clipping.

41. Use the Offstage widget

The Offstage widget allows you to hide a widget without removing it from the widget tree. This can be useful for improving performance because the framework does not need to rebuild the hidden widget.

Offstage(
offstage: !showWidget,
child: MyWidget(),
)

In this example, the Offstage widget is used to hide the MyWidget widget when the showWidget variable is false. This improves performance by reducing the number of widgets that need to be rebuilt.

Have you ever wondering what is the difference between Offstage, Opacity and Visibility widget? Here you can find short explanation.

In Flutter, the Offstage widget is used to hide a child widget from the layout while it is still part of the tree. It can be used to conditionally show or hide a child widget without having to rebuild the entire tree.

The Opacity widget is used to control the transparency of a child widget. It takes a single value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque. However, it’s important to note that it may impact performance, so use it only when necessary.

The Visibility widget is used to control the visibility of a child widget. It can be used to conditionally show or hide a child widget without having to rebuild the entire tree.

All three widgets are used to control the display of child widgets, but they do it in different ways. Offstage controls the layout, Opacity controls the transparency, and Visibility controls the visibility.

42. Use WidgetsBinding.instance.addPostFrameCallback

In some cases, we need to perform some action after the frame is rendered. Neither do not try to use any delay function, nor create custom callbacks! We can use WidgetsBinding.instance.addPostFrameCallback method to do that. This callback will be called after the frame is rendered and will improve the performance by avoiding unnecessary rebuilds.

WidgetsBinding.instance.addPostFrameCallback((_) {
//Perform the action here
});

43. Use AutomaticKeepAliveClientMixin

When using ListView or GridView, the children can be built multiple times. To avoid this, we can use AutomaticKeepAliveClientMixin for the children widgets. This will keep the state of children widgets alive and will improve the performance.

class MyChildWidget extends StatefulWidget {
@override
_MyChildWidgetState createState() => _MyChildWidgetState();
}

class _MyChildWidgetState extends State<MyChildWidget> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;

@override
Widget build(BuildContext context) {
return Text("I am a child widget");
}
}

In this example, the MyChildWidget class is using the AutomaticKeepAliveClientMixin mixin and the wantKeepAlive property is set to true. This will keep the state of the MyChildWidget alive and prevent it from being rebuilt multiple times, resulting in improved performance.

44. Use MediaQuery.sizeOf(context)

When you use MediaQuery.of(context).size, flutter associates your widget with MediaQuery’s size, which can lead to needless rebuilds when used multiple times in your codebase. By using MediaQuery.sizeOf(context), you can bypass these unwanted rebuilds and enhance your app’s responsiveness. Also applicable for other MediaQuery methods, such as using .platformBrightnessOf(context) instead .of(context).

45. ___________

(Its waiting for an Avatar, who will find and share with me. As I can only edit this blog, laugh 😆 )

Okay, But how to measure them?

To measure the performance of Dart/Flutter applications, you can use the Performance View. It offers charts to show Flutter frames and timeline events.

Don’t measure performance in debug mode
There is a special mode for performance and memory measuring, Profile mode. You can run it via IDEs like Android Studio or Visual Studio Code or by executing the following CLI command:

flutter run - profile

In Debug mode, the app is not optimised and therefore usually runs slower than in the other modes.

Don’t measure performance in an emulator
When you run your compiled app to check for performance issues, do not use an emulator. An emulator can’t reproduce real-world behavior like real devices. You might notice issues that aren’t real issues when performed on an actual device. Emulators also don’t support the Profile mode.

Conclusion

Optimising the performance of your Flutter app is crucial for delivering a seamless user experience. By implementing these tips, you can further optimize the performance of your Flutter app. Remember that performance optimization is an ongoing process, and regular profiling and testing are essential to ensure your app maintains its high-performance standards.

Thanks, guys, That’s all for now, Make sure to give a clap 👏 and leave some engagement. If there is any correction or feedback feel free to leave a comment as well.

I’m open for discussions, feel free to connect with me. Don’t worry, I don’t bite. (maybe)

Linkedin, Topmate, and Buy me a coffee !!

--

--