Flutter Performance Tip: Keep Your Build Method Pure
Improve your Flutter app's performance by keeping the build method pure and free of side effects.

Understand what build cost is and how to keep your builds pure and free of side effects.
What are Widgets?
Keep this in mind as you read this article: for Flutter to remain blazingly fast your widgets need to be created fast.

Problem Code
Lets explore an example. Keep in mind it's just an example to illustrate a point.
...
/// Not really long, just a demo.
const superLongListOfNames = ['Gordon', 'George', 'Hannah', 'Harry', 'Dan'];
/// Expensive operation, just a demo.
List<String> namesThatStartWith(String l) {
print('expensive function $l');
return superLongListOfNames
.where((element) => element.startsWith(l))
.toList();
}
class OhNoWidget extends StatelessWidget {
const OhNoWidget({
Key? key,
required this.letter,
}) : super(key: key);
final String letter;
@override
Widget build(BuildContext context) {
print('building');
List<String> filteredNames = namesThatStartWith(letter); // This is the ouchie
return ListView.builder(
itemCount: filteredNames.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: _Tile(title: filteredNames[index]),
);
},
);
}
}
...
Above you have a StatelessWidget; this widget takes in a String property, called letter
.
You use this property to filter a long list of names that start with that letter. You have a function called namesThatStartWith
and you filter the list superLongListOfNames
.
You call this function in the build
method, and then you use the result to display the names that start with that letter.
There are two problems with this widget.
- If you assume this list is really long, then you should probably do this operation in a separate isolate instead. We'll talk about isolates in a future article.
- We're calling
namesThatStartWith
from the widget'sbuild
method; this is the main issue that we'll focus on in this article.
This means this operation will be performed every time the build method is called. Meaning any time a parent is updated, or new (unrelated) state is passed in, or the user navigates the app. The build method is supposed to be called a lot, and that is part of how Flutter is designed. Keep in mind that you don't always have control over when a build is triggered.
Here is an example of what this output would look like:

There's an extra button to trigger a call to setState
higher up in the widget tree.
When that button is tapped the build
method will be called again and as a side effect the expensive function will be called as well.
Solution
Keep your build method pure and free of side effects!
In this example you can fix the issue by making this a StatefulWidget, and moving the call to the expensive function inside initState
.
class OhNoWidget extends StatefulWidget {
const OhNoWidget({
Key? key,
required this.letter,
}) : super(key: key);
final String letter;
@override
_OhNoWidgetState createState() => _OhNoWidgetState();
}
class _OhNoWidgetState extends State<OhNoWidget> {
late List<String> filteredNames;
@override
void initState() {
super.initState();
filteredNames = namesThatStartWith(widget.letter); // Moved here
}
@override
Widget build(BuildContext context) {
print('building');
return ListView.builder(
itemCount: filteredNames.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: _Tile(title: filteredNames[index]),
);
},
);
}
}
And there you go 👍🏼. Now this will only be called the first time the widget is created, and not for each build.
Point is to keep your builds clean and not to perform expensive work directly in the build. Extract that logic to whatever form of state management you use.
Handling State Updates
The logic would look different depending on how you manage your state and trigger rebuilds in your app.
For this particular example when using setState
, take note that if the letter
property you pass in were to change, then you'd need to filter the list again.
You can do that by overriding didUpdateWidget
.
Just add a check to see if the oldWidget.letter
value and the new widget.letter
value are different. If yes, recompute and set the local filteredNames
state.
@override
void didUpdateWidget(OhNoWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.letter != widget.letter) {
filteredNames = namesThatStartWith(widget.letter);
}
}
The main point is that you only want to be recomputing when necessary.
That's that!
Code fast, Flutter faster.