Raising the bar for Flutter App performance!

Parth Bhanderi
7 min readJun 19, 2023

--

Flutter apps often perform well by default, so all you need to do to achieve outstanding performance is to avoid common errors. You can write the most effective Flutter app possible with the aid of these best practices.

So let’s get started without further ado.

  1. Use const / static

You’ve probably encountered a lot of examples where the const keyword should always be used. What gives, though?

Consider the following case. We have a method called doSomething, and in that function, we are producing two instances of a class by executing a basic for loop 100 times.

Observe that the constructor of the ConstObject class is designated as const, but the constructor of the ConstObjectNot class is not.

void doSomething(){
for(var i = 0; i < 100; i++0){
const constobject = Constobject();
final notConstobject = ConstobjectNot();
}
}

class Constobject {
const Constobject();
}

class ConstobjectNot {
ConstobjectNot();
}

Now, if we run this in our app, the object for the constobject will be created just once, even if the loop was executed 100 times, however, the object for the notConstobject would be created 100 times because the const keyword was non-used there.

A widget will only be produced once over its lifetime if it is made const. As a result, it will help you conserve memory and improve the functionality of your program. Even if we call the set state on the widget, nothing will change.

This may be applied to any Object in Flutter, not only Widgets.

However, there are instances when we are unable to declare a variable or function const, thus in those cases, you may use static keywords for non-constant objects. When you can’t use const, the function or variable tagged as constant will only be generated once, saving you memory.

Consider the following scenario: Suppose you want to construct a file in Dart that has all the get, post, put, and delete functions for a rest API. Therefore, there should only be one instance of that class in the entire program. You can designate such methods as static as a result.

2. Delay execution when possible:

Consider the following scenario:

bool condition = Random().nextBool();
late final operationOne = someExpensiveOperaion();
late final operationTwo = evenMoreExpensiveOperation();

bool someExpensiveOperaion() {
//consider this will takes a lot of time
return Random().nextBool();
}

bool evenMoreExpensiveOperation() {
//consider this will takes even lot of time
return Random().nextBool();
}

void doSomething() {
if(condition && operationOne && operaitonTwo) {
print('Hello World.');
}
}
  • First, we’re going to create three variables, two of which are late-type variables.
  • Then, we check three criteria in the doSomething() method.
  • If the first condition is true, we just want to do operation 1 check. As a result, only if the first condition is true will the operationOne and operationTwo late type variables be calculated.
  • As a result, you will save some money on computation expenses.

If you’re not familiar with the late keyword, it basically indicates that it will only be initialized when we access it.

3. Use Stateless/Stateful widgets over functions.

Consider these 2 code snippets:

 Padding buildPadding() {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
);
}
class CustomePadding extends StatelessWidget {
const CustomePadding({super.key});

@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
);
}
}

When a layout is vast, we often divide it out using methods for each widget. However, if a widget is extracted and used as a method, it is regarded as a very undesirable pattern (anti-pattern) for the following reasons:

  1. We will waste CPU cycles if you are in a Stateful Widget when the set state is activated since it will also refresh the widgets we have within the procedure.
  2. You won’t be able to tell your extracted method apart if you look at the widget tree in the Dart developer tools.

Therefore, never return them in methods; always extract them in Stateless / Stateful Widget.

4. Avoid rebuilding unnecessary widgets inside AnimatedBuilder

Always add your widget to the kid parameter as shown below whenever you wish to utilize AnimatedBuilder for your animations.

  • Not in this manner:
body: AnimatedBuilder(
animation: controller,
builder: (_, child) => Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(360 * _controller.value * (pi / 180.0)),
child: FooWidget(counter: counter),
),
),
  • Instead, employ it as follows:
body: AnimatedBuilder(
animation: controller,
child: CounterWidget(counter: counter),
builder: (_, child) => Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(360 * _controller.value * (pi / 180.0)),
child: child,
),
),

We made advantage of the AnimatedBuilder’s child property, which enables us to cache widgets for later usage in our animation.

We do this action since that widget won’t change its only function will be rotation, and we already have the Transform widget for that.

5. Precaches your images:

It can occasionally take some time to load a photo from assets into your Flutter app, but by precaching the pictures in your memory, they will load as rapidly as possible.

The snippet of code for preaching your photos is provided below:

Image image;

@override
void initState() {
super.initState();
image = Image.asset(path);
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(image.image, context);
}

Note: Flutter offers the precacheImage method; if you wish to cache SVG, use the precache function of the flutter_svg package.

6. Don’t use ClipRRect / Opacity widget when not necessary:

You could believe that utilizing ClipRRect or Opacity won’t slow down your app, however in order to demonstrate the differences between the two, we’ll perform the following:

  • Consider the following code snippets:
class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
color: Colors.red,
),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
);
}
}

Now execute the first code in profile mode and enter the following command in the terminal:

flutter screenshot — type=skia — observatory-url=[url]

A skia screenshot will be produced and placed in the root of your application directory. Go to this website now and put the screenshot of Skia there. It will display something similar.

  • You can see that the save function is being called several times here, which uses a lot of memory and processing resources (see 1.0).
  • While utilizing BoxDecoration of Container, which is less costly, in place of ClipRRect in the second example, we are still specifying border-radius.
  • Additionally, we can observe that the save layer is only called twice.

Another usage scenario for the Opacity widget is as follows:

class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
child: Opacity(
opacity: 0.5,
child: Container(
color: Colors.red,
),
),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
child: Container(
color: Colors.red.withOpacity(0.5),
),
);
}
}
  • As you can see, we can simply apply opacity to color rather than wrapping a widget with opacity, which is more performance-optimized because it avoids repeatedly calling the save layer method.
  • To view the intricacies of it, you may take a screenshot of it in Skia and test it out in the debugger.

7. Use nil package:

We utilize Container or SizedBox if we wish to display something on the screen or return null. However, what actually occurs is that a Render Object, which is not visible on the screen but still consumes memory, is created by the two widgets.

This package just generates elements; render objects are not generated by it. By utilizing this package, you may prevent the production of pointless rendering objects. Performance will improve and memory will be saved.

Here is an illustration of how to apply it:

import 'package:nil/nil.dart';

return Builder(
builder: (_) {
if(condition) {
return const MyWidget();
} else {
return nil;
}
}
);

You may also return const Nil() in place of returning nil.

However, there is one item to consider. You should not use nil in Column or ListView since this package just constructs an element, not a Render object. Instead, use Sizedbox. If you wish to utilize it inside of a Column or ListView, use shrink().

Although my posts are free, did you know that you may clap 50 times? The more successful you become, the more inspired I am to write more for you guys.

For more fascinating information like this, be sure to follow Parth Bhanderi!

--

--

Parth Bhanderi

Flutter | Dart Virtuoso!!! Go where your energy is appreciated!