Filip Hráček / Flutter performance /
WARNING: This chapter is a stub.
Your code doesn’t run alone, like some ethereal idea in a perfectly uneventful void.
Instead, it runs in an onion.
Fancy performance engineers generally call this “the environment”. I call it an onion.
{ DIAGRAM of the onion }
The onion has layers, and each of them affects your code’s performance. Let’s start from the inside-most layer.
Much of your Flutter code is programmed in a declarative style, and it’s on the framework to figure out how to make things work. This in itself is a bit of a layered onion — from C++ code of the Flutter engine all the way up to common Flutter widgets such as ListView
— but you get it as an atomic package. When you upgrade Flutter, the new version contains a specific version of Dart, a specific Flutter engine, and so on. It’s rare for people to be running, say, Flutter 3.29.0 with anything else than framework revision 35c388afb5, engine revision f73bfc4522, and Dart 3.7.0.
The version of the Flutter SDK can obviously have a large effect on the performance of your code. It’s common for Flutter to get performance improvements even in minor version bumps. It’s also very much possible to encounter a performance regression from version to version.
When evaluating performance, always be aware which version of the Flutter SDK your app is written in. You can find the version by running flutter --version
. You can also run flutter --version --machine
to get machine-readable JSON output like this:
{
“frameworkVersion”: “3.29.0”,
“channel”: “stable”,
“repositoryUrl”: “https://github.com/flutter/flutter.git”,
“frameworkRevision”: “35c388afb57ef061d06a39b537336c87e0e3d1b1”,
“frameworkCommitDate”: “2025-02-10 12:48:41 -0800”,
“engineRevision”: “f73bfc4522dd0bc87bbcdb4bb3088082755c5e87”,
“dartSdkVersion”: “3.7.0”,
“devToolsVersion”: “2.42.2”,
“flutterVersion”: “3.29.0”,
“flutterRoot”: "/path/to/flutter"
}
If you need to access this information from within a running app, use FlutterVersion
.
import 'package:flutter/services.dart' show FlutterVersion;
var version = FlutterVersion.version;
When you run dart compile exe
— either directly or through flutter build
— your Dart code gets compiled into machine instructions.
So instead of
var x = z + 1;
you get something like
mov rdi, 0x1
add rax, rdi
except in binary, so it’s something like
01001000100000111100011100000001010010001000100111111000
in the actual .exe
file.
But that’s not all that goes into that file. The compiler must also include the Dart runtime. That’s all the things that your program does that you don’t need to worry about. For example:
List
in Dart, you don’t need to worry about allocating memory for it, nor do you have to remember to free the memory once the list is no longer needed. But your code wouldn’t work without this, so it’s included in the .exe
as part of the runtime. This includes the garbage collector, which will be a recurring character in this book.StackTrace
object, or finding whoever’s responsible of catching it. But again, your code wouldn’t work without this, so it’s included in the .exe
as part of the Dart runtime.file.readAsString()
, you don’t need to worry about interfacing with the underlying system. Same goes for any other input or output. This, too, goes into your binary as part of the Dart runtime.The binary code you ship is deeply intertwined with the Dart runtime. Some of the most basic operations — things that you never ever think about while writing Dart, such as passing objects to functions — are performed by the runtime.
So, as you can imagine, the Dart runtime has a major effect on the performance of your code.
You can find you installed Dart’s version by running dart --version
. The version is also printed with flutter --version
.
The runtime will be different for different operating systems and architectures. So, the runtime for linux_arm64
will be different from the one for windows_x64
even if they are the same version number. But we’ll get back to this further along our path through the onion.
If you need to find out the version of the runtime from within a running Dart program, you can use the following:
import 'dart:io' show Platform;
var version = Platform.version;
This assumes you’re not running the program in the browser. Then again, if you’re running Dart in the browser, chances are you’re running a Flutter app, and you can use the aforementioned FlutterVersion
class and its FlutterVersion.dartVersion
member.
The browser is a whole separate layer, of course. When compiling for the Web, your Dart code and the Dart runtime is either compiled into JavaScript or into Web Assembly. The browser then executes that JavaScript or Web Assembly. Suffice to say, the browser and its version matters. Plus, unlike the runtime that is compiled with your code, the browser environment can (and will) change “under your feet.” Browser vendors (Google, Microsoft, Mozilla, Apple) constantly work on their technology. This means that the same build of your app (the same compiled blob of JavaScript or Web Assembly) can run at different speeds today than they will do tomorrow.
Let’s get back to your compiled .exe
with all its machine instructions. Not even this can run by itself. It needs an operating system to talk to. The OS provides access to files, the network, and important services such as the time. It also allocates memory, provides processing threads, starts processes, and more. Any of these things can be more efficient, or less efficient, depending on the OS and its version.
So, once again, you should remember to capture this context when analyzing performance. To get the information about the exact version of the operating system, use the Platform
class:
import 'dart:io' show Platform;
var osName = Platform.operatingSystem;
var osVersion = Platform.operatingSystemVersion;
But the real issue with operating systems isn’t that their performance changes slightly between versions. It’s that they constantly and actively affect performance in real time.
We talked about governors in the previous chapter about Performance lottery. In short, operating systems change processing speed depending on various heuristics. If the OS governor thinks that the current load isn’t urgent, it lowers the clock speed of a processor or even puts some CPUs to sleep. This can happen really quickly: one millisecond, the device is firing on all cylinders, and the next one, it’s in battery saving mode. Then a fraction of a second later, the user touches the screen, and the governor switches everything back on.
Governors are a pain when it comes to benchmarking but they’re obviously a good thing from the more general perspective of app performance. Yes, we want our apps to be snappy, but not at the cost of draining all of the user’s battery, or keeping their cooling fans on at all times. Once again, it’s a tradeoff.
We’ll talk about how to disable governors in a later chapter. For now, let’s just acknowledge that these heuristics, which are included in all our operating systems (from desktop to laptops to mobile phones), have an effect on the performance of our programs.
We live in a world of thousands of devices from hundreds of manufacturers, all of them with slightly different performance characteristics. A 13-inch 2020 MacBook Pro M1 is different from a custom-build Intel x64 PC is different from Samsung A25 6GB is different from Samsung A25 8GB is different from iPhone 16e is different from iPhone 16 Pro Max 256GB is different from Raspberry Pi 4B.
The great thing is that Flutter can target each of these devices and a thousand more.
The absolutely terrifying thing is that Flutter can target each of these devices and a thousand more.
Well, it’s only terrifying if you expect your app’s performance to be precisely the same on your user’s devices as it is on yours. Once you let go of this silly notion, you’re good.
In practical terms, it’s important to test your app on a wide variety of devices. Don’t do the Silicon Valley thing where people only ever see their app running on the latest flagship phone connected to spotless 5G. Ideally, you should have your app on your daily-driver phone — so that the app needs to co-exist with a myriad other apps and services — and that phone should be on the lower end of your target audience’s device spectrum.
If you’re really into shiny new iPhones but you’re shipping apps that will be used by hundreds of millions of users in emerging markets (or just ordinary users in the US), I have bad news for you. You have two options:
If you’re benchmarking, select a single device and use that. Comparing two benchmark results from two different devices is useless.
When I was employed at Google on the Flutter team, my family and I lived in Mountain View, California. We rented an apartment under the roof and so, during the summer, it got really hot. (We didn't have air conditioning. We’re one of those weird types who avoid air conditioning unless it’s unbearably hot. And in Silicon Valley, it’s almost never unbearably hot.)
Then the pandemic came, and we had to work from home. I took the bedroom.
I had an Intel MacBook Pro back then, and I remember when the first really hot day came...