Splitting widgets to methods is a performance anti-pattern
Originally published at iirokrankka.com on December 11, 2018.
It has been almost 6 months since I wrote an article on cleaning up Flutter UI code.
The article had several tips on how to organize your Flutter UI code for less clutter and more readability. And it’s still a quite popular article. However, there was this one tip there that advocated doing something that didn’t turn out the way I thought it would.
To escape the Lisp-y bracket hell, I advocated for splitting long build methods into multiple separate smaller methods. The following code sample is entirely nonsensical, but we’ll pretend that this is a snippet that does something useful.
So, for example, if we have a widget that looks something like this:
Looking at the code, you might get a sense that the nesting level gets a little crazy, and it does.
It would be awesome to reduce the indentation level a little bit. Since widgets can be a little boilerplatey, the first solution that comes into mind is to split the nested part into a separate method.
At first we might be tempted to try something like this:
Problem solved, right? Time to call it a day and go home!
Well, not quite.
The problem with splitting widgets into methods
At first glance, splitting long build methods into small functions makes perfect sense. And you can certainly see this pattern used in the wild a lot. It’s also in heavy use in our codebase at Reflectly — and believe me, we have a bunch of UI code.
Now, 6 months later after my initial article, I’m here to tell you that you shouldn’t do this. If you picked up this habit after reading my article, you might be a little pissed at me right now. And you’re welcome, this is what friends do for each other! (I’m very sorry.)
The issue with this approach was first brought to my attention by Wm Leler, in the comment section of the aforementioned article.
Wm’s comment explained how splitting widgets into functions is a performance antipattern.
For those who don’t already know, Wm is a Flutter developer advocate at Google.
Some of you who read Wm’s comment will have an “A-ha” moment right now, some of you won’t. I didn’t, and that’s fine; let’s dig into what’s going on here and see if we can’t make that moment happen by the time you finish this article.
So what’s the problem, really?
Whenever the value of
_counter changes, the framework calls the
build method. This triggers our widget to rebuild itself. The problem is that
_buildNonsenseWidget() gets called every time the value of
_counter changes - which ends up rebuilding the widget tree over and over again.
Rebuilding for nothing
In this case, there’s no reason to rebuild that particular widget tree.
The widget tree returned by
_buildNonsenseWidget() is stateless by nature - we only need to build it once. Sadly, because the widget tree is built by the
_buildNonsenseWidget() method, the Flutter framework rebuilds it every time when the parent widget rebuilds.
Essentially, we’re wasting precious CPU cycles in rebuilding something that doesn’t need to be rebuilt. This happens because from the framework’s perspective, there’s no difference between a long-assed build method and one that’s been split into multiple, smaller methods. Mind you, this is only a simple example — this has a more significant impact on more complex apps.
Splitting long build methods — revisited
The solution for this one is relatively simple, although it results in a couple of extra lines of code. Instead of splitting build methods into smaller methods, we split them into widgets — StatelessWidgets, that is.
When we refactor the previous example, we’ll end up with this:
While it’s a little more code, this is much better.
_NonsenseWidget is built only once and all of the unnecessary rebuilds are gone. The parent widget can rebuild itself multiple times, but the
_NonsenseWidget doesn’t care - it’s built once and once only.
(This is only one part of the story and applies to
const StatelessWidgets - see this answer for what is the difference between functions and classes to create widgets? - a StackOverflow answer by Remi Rousselet.)
Splitting widgets into smaller widgets — more complex examples
You might be thinking that the above was a really simple example and it doesn’t represent the complexity of a real app.
You’d be right, again. I recently updated the open source inKino app to follow the advice in this article. For example, I think this is a good example of splitting widgets into smaller
StatelessWidgets in a bigger app.
Instead of splitting you build methods into multiple smaller methods, split them into
StatelessWidgets. This way, you won’t be rebuilding your static widget trees multiple times that do nothing but waste CPU cycles. When it comes to optimizing performance of Flutter apps, this is probably one of the lowest hanging fruits.
If you really prefer building your widget trees with methods, you might want to take a look at a package called functional_widget by Remi Rousselet. It alleviates the problems that come with building widget trees with methods by using code generation.