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