Flutter Performance Optimization Best Practices
In the competitive mobile landscape, a seamless and responsive user experience isn’t just a perk, it’s a necessity. Yet, Flutter developers often face the challenge of crafting performant apps, battling against potential lag, stutters, and sluggish response times. There are certain best practices that can be followed to improve performance even further.
Use the latest version of Flutter
The latest version of Flutter always includes the latest performance optimizations. Therefore, it is important to keep your Flutter SDK up to date.
Avoid using debug mode
Debug mode in Flutter is useful for debugging code, but it can also significantly impact performance. Therefore, it is important to avoid using debug mode in production.
Use the right widgets for the job
Flutter offers a wide variety of widgets that can be used to build user interfaces. It is important to choose the right widget for the job, as some widgets are more performant than others. For example, a ListView
is more performant than a GridView
.
Avoid using unnecessary rebuilds
When the state of an object changes, Flutter will automatically rebuild the widget that uses that object. However, if a widget is rebuilt unnecessarily, it can impact performance. Therefore, it is important to avoid using unnecessary rebuilds.
Run in profile mode for performance profilling
Flutter’s profile mode compiles and launches your application almost identically to release mode, but with just enough additional functionality to allow debugging performance problems. For example, profile mode provides tracing information to the profiling tools.
Note: DevTools can’t connect to a Flutter web app running in profile mode. Use Chrome DevTools to generate timeline events for a web app.
Use the DevTools
The Flutter DevTools is a tool that can be used to debug and profile Flutter apps. The devtools can be used to view the performance of your app in real time, and to identify potential performance bottlenecks.
Test your app on real devices
It is important to test your app on real devices to get a realistic view of its performance. Emulators and simulators can be useful for development, but they may not accurately reflect the performance of your app on real devices.
Monitor your app’s performance
Once you have released your app, it is important to monitor its performance. This will help you to identify any performance issues that may arise, and to take steps to address them.
Minimize expensive operations
Some operations are more expensive than others, meaning that they consume more resources. Obviously, you want to only use these operations when necessary. How you design and implement your app’s UI can have a big impact on how efficiently it runs.
Control build() cost
Here are some things to keep in mind when designing your UI:
- Avoid repetitive and costly work in
build()
methods sincebuild()
can be invoked frequently when ancestor widgets rebuild. - Avoid overly large single widgets with a large
build()
function. Split them into different widgets based on encapsulation but also on how they change:- When
setState()
is called on aState
object, all descendent widgets rebuild. Therefore, localize thesetState()
call to the part of the subtree whose UI actually needs to change. Avoid callingsetState()
high up in the tree if the change is contained to a small part of the tree. - The traversal to rebuild all descendents stops when the same instance of the child widget as the previous frame is re-encountered. This technique is heavily used inside the framework for optimizing animations where the animation doesn’t affect the child subtree. See the TransitionBuilder pattern and the source code for SlideTransition, which uses this principle to avoid rebuilding its descendents when animating. (“Same instance” is evaluated using operator ==, but see the pitfalls section at the end of this page for advice on when to avoid overriding operator ==.)
- Use
const
constructors on widgets as much as possible, since they allow Flutter to short-circuit most of the rebuild work. To be automatically reminded to useconst
when possible, enable the recommended lints from the flutter_lints package. - To create reusable pieces of UIs, prefer using a StatelessWidget rather than a function.
- When
For more information, check out:
- Performance considerations, part of the StatefulWidget API doc
- Widgets vs helper methods, a video from the official Flutter YouTube channel that explains why widgets (especially widgets with
const
constructors) are more performant than functions.
Use saveLayer() thoughtfully
Some Flutter code uses saveLayer()
, an expensive operation, to implement various visual effects in the UI. Even if your code doesn’t explicitly call saveLayer(), other widgets or packages that you use might call it behind the scenes. Perhaps your app is calling saveLayer()
more than necessary; excessive calls to saveLayer()
can cause jank.
Why is saveLayer expensive?
Calling saveLayer()
allocates an offscreen buffer and drawing content into the offscreen buffer might trigger a render target switch. The GPU wants to run like a firehose, and a render target switch forces the GPU to redirect that stream temporarily and then direct it back again. On mobile GPUs this is particularly disruptive to rendering throughput.
When is saveLayer required?
At runtime, if you need to dynamically display various shapes coming from a server (for example), each with some transparency, that might (or might not) overlap, then you pretty much have to use saveLayer()
.
Debugging calls to saveLayer
How can you tell how often your app calls saveLayer()
, either directly or indirectly? The saveLayer()
method triggers an event on the DevTools timeline; learn when your scene uses saveLayer
by checking the PerformanceOverlayLayer.checkerboardOffscreenLayers
switch in the DevTools Performance view.
Minimizing calls to saveLayer
Can you avoid calls to saveLayer
? It might require rethinking of how you create your visual effects:
- If the calls are coming from your code, can you reduce or eliminate them? For example, perhaps your UI overlaps two shapes, each having non-zero transparenc
- If they always overlap in the same amount, in the same way, with the same transparency, you can precalculate what this overlapped, semi-transparent object looks like, cache it, and use that instead of calling
saveLayer()
. This works with any static shape you can precalculate. - Can you refactor your painting logic to avoid overlaps altogether?
- If they always overlap in the same amount, in the same way, with the same transparency, you can precalculate what this overlapped, semi-transparent object looks like, cache it, and use that instead of calling
- If the calls are coming from a package that you don’t own, contact the package owner and ask why these calls are necessary. Can they be reduced or eliminated? If not, you might need to find another package, or write your own.
Other widgets that might trigger saveLayer() and are potentially costly:
- ShaderMask
- ColorFilter
- Chip—might trigger a call to
saveLayer()
ifdisabledColorAlpha != 0xff
- Text—might trigger a call to
saveLayer()
if there’s anoverflowShader
Minimize use of opacity and clipping
Opacity is another expensive operation, as is clipping. Here are some tips you might find to be useful:
- Use the Opacity widget only when necessary. See the Transparent image section in the
Opacity
API page for an example of applying opacity directly to an image, which is faster than using theOpacity
widget. - Instead of wrapping simple shapes or text in an
Opacity
widget, it’s usually faster to just draw them with a semitransparent color. (Though this only works if there are no overlapping bits in the to-be-drawn shape.) - To implement fading in an image, consider using the FadeInImage widget, which applies a gradual opacity using the GPU’s fragment shader. For more information, check out the Opacity docs.
- Clipping doesn’t call
saveLayer()
(unless explicitly requested withClip.antiAliasWithSaveLayer
), so these operations aren’t as expensive as Opacity, but clipping is still costly, so use with caution. By default, clipping is disabled (Clip.none
), so you must explicitly enable it when needed. - To create a rectangle with rounded corners, instead of applying a clipping rectangle, consider using the
borderRadius
property offered by many of the widget classes.
Implement grids and lists thoughtfully
How your grids and lists are implemented might be causing performance problems for your app. This section describes an important best practice when creating grids and lists, and how to determine whether your app uses excessive layout passes.
Be lazy!
When building a large grid or list, use the lazy builder methods, with callbacks. That ensures that only the visible portion of the screen is built at startup time.
For more information and examples, check out:
- Working with long lists in the Cookbook
- Creating a ListView that loads one page at a time: a community article by AbdulRahman AlHamali
- Listview.builder API
Avoid intrinsics
For information on how intrinsic passes might be causing problems with your grids and lists, see the next section.
Minimize layout passes caused by intrinsic operations
If you’ve done much Flutter programming, you are probably familiar with how layout and constraints work when creating your UI. You might even have memorized Flutter’s basic layout rule: Constraints go down. Sizes go up. Parent sets position.
For some widgets, particularly grids and lists, the layout process can be expensive. Flutter strives to perform just one layout pass over the widgets but, sometimes, a second pass (called an intrinsic pass) is needed, and that can slow performance.
What is an intrinsic pass?
An intrinsic pass happens when, for example, you want all cells to have the size of the biggest or smallest cell (or some similar calculation that requires polling all cells).
For example, consider a large grid of Cards. A grid should have uniformly sized cells, so the layout code performs a pass, starting from the root of the grid (in the widget tree), asking each card in the grid (not just the visible cards) to return its intrinsic size—the size that the widget prefers, assuming no constraints. With this information, the framework determines a uniform cell size, and re-visits all grid cells a second time, telling each card what size to use.
Debugging intrinsic passes
To determine whether you have excessive intrinsic passes, enable the Track layouts option in DevTools (disabled by default), and look at the app’s stack trace to learn how many layout passes were performed. Once you enable tracking, intrinsic timeline events are labeled as $runtimeType intrinsics
.
Avoiding intrinsic passes
You have a couple options for avoiding the intrinsic pass:
- Set the cells to a fixed size up front.
- Choose a particular cell to be the “anchor” cell—all cells will be sized relative to this cell. Write a custom render object that positions the child anchor first and then lays out the other children around it.
To dive even deeper into how layout works, check out the layout and rendering section in the Flutter architectural overview.
Build and display frames in 16ms
Since there are two separate threads for building and rendering, you have 16ms for building, and 16ms for rendering on a 60Hz display. If latency is a concern, build and display a frame in 16ms or less. Note that means built in 8ms or less, and rendered in 8ms or less, for a total of 16ms or less.
If your frames are rendering in well under 16ms total in profile mode, you likely don’t have to worry about performance even if some performance pitfalls apply, but you should still aim to build and render a frame as fast as possible. Why?
- Lowering the frame render time below 16ms might not make a visual difference, but it improves battery life and thermal issues.
- It might run fine on your device, but consider performance for the lowest device you are targeting.
- As 120fps devices become more widely available, you’ll want to render frames in under 8ms (total) in order to provide the smoothest experience.
If you are wondering why 60fps leads to a smooth visual experience, check out the video Why 60fps?
Pitfalls
If you need to tune your app’s performance, or perhaps the UI isn’t as smooth as you expect, the DevTools Performance view can help!
Also, the Flutter plugin for your IDE might be useful. In the Flutter Performance window, enable the Track Widget Builds check box. This feature helps you detect when frames are being rendered and displayed in more than 16ms. Where possible, the plugin provides a link to a relevant tip.
The following behaviors might negatively impact your app’s performance.
- Avoid using the
Opacity
widget, and particularly avoid it in an animation. UseAnimatedOpacity
orFadeInImage
instead. For more information, check out Performance considerations for opacity animation. - When using an
AnimatedBuilder
, avoid putting a subtree in the builder function that builds widgets that don’t depend on the animation. This subtree is rebuilt for every tick of the animation. Instead, build that part of the subtree once and pass it as a child to theAnimatedBuilder
. For more information, check out Performance optimizations. - Avoid clipping in an animation. If possible, pre-clip the image before animating it.
- Avoid using constructors with a concrete
List
of children (such asColumn()
orListView()
) if most of the children are not visible on screen to avoid the build cost. - Avoid overriding
operator ==
onWidget
objects. While it might seem like it would help by avoiding unnecessary rebuilds, in practice it hurts performance because it results in O(N²) behavior. The only exception to this rule is leaf widgets (widgets with no children), in the specific case where comparing the properties of the widget is likely to be significantly more efficient than rebuilding the widget and where the widget will rarely change configuration. Even in such cases, it is generally preferable to rely on caching the widgets, because even one override ofoperator ==
can result in across-the-board performance degradation as the compiler can no longer assume that the call is always static.
Performance FAQ
-
Which performance dashboards have metrics that are related to Flutter?
-
How do I add a benchmark to Flutter?
-
What are some tools for capturing and analyzing performance metrics?
-
My Flutter app looks janky or stutters. How do I fix it?
-
What are some costly performance operations that I need to be careful with?
Opacity
,Clip.antiAliasWithSaveLayer
, or anything that triggerssaveLayer
ImageFilter
-
How do I tell which widgets in my Flutter app are rebuilt in each frame?
- Set
debugProfileBuildsEnabled
true in widgets/debug.dart. - Alternatively, change the
performRebuild
function in widgets/framework.dart to ignoredebugProfileBuildsEnabled
and always callTimeline.startSync(...)/finish
. - If you use IntelliJ, a GUI view of this data is available. Select Track widget rebuilds, and your IDE displays which the widgets rebuild.
- Set
-
How do I query the target frames per second (of the display)?
-
How to solve my app’s poor animations caused by an expensive Dart async function call that is blocking the UI thread?
- Spawn another isolate using the
compute()
method, as demonstrated in Parse JSON in the background cookbook.
- Spawn another isolate using the
-
How do I determine my Flutter app’s package size that a user will download?
-
How do I see the breakdown of the Flutter engine size?
- Visit the binary size dashboard, and replace the git hash in the URL with a recent commit hash from GitHub engine repository commits.
-
How can I take a screenshot of an app that is running and export it as a SKP file?
- Run
flutter screenshot --type=skia --observatory-uri=...
- Note a known issue viewing screenshots:
- Issue 21237: Doesn’t record images in real devices.
- To analyze and visualize the SKP file, check out the Skia WASM debugger.
- Run
-
How do I retrieve the shader persistent cache from a device?
-
On Android, you can do the following:
1 2 3 4 5 6 7
adb shell run-as <com.your_app_package_name> cp <your_folder> <some_public_folder, e.g., /sdcard> -r adb pull <some_public_folder/your_folder>
-
-
How do I perform a trace in Fuchsia?