How do you design a Flutter app to most efficiently render your scenes? In particular, how do you ensure that the painting code generated by the framework is as efficient as possible? Some rendering and layout operations are known to be slow, but can’t always be avoided. They should be used thoughtfully, following the guidance below.
Rendering time of each widget isn't going to be same accross the framework, right? Similarly, there's few operations which are expensive interms of CPU & Memory utilizations. If we can minimze or avoid them then the performance of the app will be good.
-
Avoid/Minize any piece of code that is causing the re-builds (i.e., Calling the
build()
method again & again), since build can be invoked frequently when ancestor widget rebuild. -
Use
const
keyword where ever it is possible to the widgets, So that flutter automatically avoids the re-build of those widgets. -
Avoid large
build()
methods since if we have to callsetState()
then all it's child widgets will also get re-built, so try to split as much as possible & have the setState local to where it is required.@override Widget build(BuildContext context) { return Column( children: [ Container(...), // where we need re-build ElevatedButton(...), const Text('Performance'), Expanded(...), // We need re-build here in other use-case ], ); } // using `setState()` here will re-build all it's child widgets even if not required. // Converting the above long build method by splitting into smaller ones & having the setState local will avoid re-build of other widgets. @override Widget build(BuildContext context) { return Column( children: [ ChildClass1(), // we can have setState just local to this class ChildClass2(), const Text('Performance'), Expanded(child:ChildClass3()), // setState local to this class ], ); }
-
saveLayer()
is an expensive operation. To implement various visual effects in the UI, we use this. Excessive calls to it can cause jank. -
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. -
Know more about it (On when it is required, how to debug calls to it etc.), here.
-
If the calls are coming from your code, then try to reduce or eliminate them.
-
For example, as shown in the below picture, let's consider our UI overlaps two circles, each having non-zero transparency.
-
If they always overlap in the same amount, in the same way, with the same transparency, we 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. -
If we don't need any overlap then we can change our paint logic or we avoid the widgets that call
saveLayer()
. -
Avoid/reduce the use of
Stack
. -
Following are few more widgets that might call
saveLayer()
- ShadeMask
- ColorFilter
Chip
ifdisabledColorAlpha != 0xff
Text
if there’s anoverflowShader
Clipping
when type isClip.antiAliasWithSaveLayer
- ...
-
Animating an
Opacity
widget directly causes the widget (and possibly its subtree) to rebuild each frame, which is not very efficient. Consider using anAnimatedOpacity
or aFadeTransition
instead. -
Directly drawing an
Image
orColor
with opacity is faster than usingOpacity
on top of them becauseOpacity
could apply the opacity to a group of widgets and therefore a costly offscreen buffer will be used. Drawing content into the offscreen buffer may also trigger render target switches and such switching is particularly slow in older GPUs.// Bad way // Example-1 Opacity( opacity: 0.5, child: const Image.network( "https://avatars.githubusercontent.com/u/46712434?v=4", color: Colors.white, colorBlendMode: BlendMode.modulate ), ) // Example-2 Opacity(opacity: 0.5, child: Container(color: Colors.transparent)) // Good Way // Example-1 Image.network( "https://avatars.githubusercontent.com/u/46712434?v=4", color: const Color.fromRGBO(255, 255, 255, 0.5), colorBlendMode: BlendMode.modulate ) // Example-2 Container(color: Colors.transparent.withOpacity(0.5))
-
To implement fading in an image, consider using the
FadeInImage
widget, which applies a gradual opacity using the GPU’s fragment shader, so it won't effect the performance & won't cause jank. -
To create a rounded rectangular corners to card, instead of applying a clipping rectangle, consider using the borderRadius property offered by many of the widget classes.
// Bad way ClipRRect( borderRadius: BorderRadius.circular(10), child: const Card(...), ) // Good way Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ... )
-
Be lazy when working with lists & grids.
-
Avoid
shrinkWrap
property (By default it will be false), because this converts List to Column, meaning we are not getting benifits of the lazy loading here. As the List waits for all of it's children to render before it show up on the screen. -
Use pagination if the lists are longer.
-
Use
ListView.builder()
wherever possible.ListView( shrinkWrap: true, // Make it false to use lazy loading children: [ Child1(), Child2(), ... ] ) // Above code snippet behaves exactly same as below code snippet // Not getting the actual benifits of ListView Column( children: [ Child1(), Child2(), ... ], )
-
Minimze the intrinsic passes (Especially in Layout of List & Grid).
-
If we have to allocate the width & height to a grid item or to a list item based on some constraints like bigger grid's size need to be allocated to all other grid items. In such cases, layout will get size of each grid starting from root. Once it get's all the sizes then finds the maximum one & passes the maximum one again back to all items causing a re-build which degrades the performance.
-
To dig depper into the layouts, click here.
-
Build and display frames in 16ms or less. There's two seperate threads for building & rendering. Build & render together in total shouldn't cross 16ms for better performance.
-
Targeting even lower might not show any visual impacts but improves on other factors like batter life, thermal issues.
// Here's some bench-mark values for smoother performance // 120fps - 8ms or lesser // 60fps - 16ms or lesser
-
Findout why 60fps leads smooth visual experience even with 16ms from resources section.
-
AnimationBuilder
rebuilds it's child sub-tree for each tick. Which again backs to our first point. To avoid this, Takeout the static or non-dependent part, create child once & pass this child to builder, so it won't rebuild again & again. -
Avoid clipping in an animation. If possible, pre-clip the image before animating it.
-
Reduce the number of isloates that are being creating in parallel. Too many isolates will cause memory jank since for each isolate that we create flutter allocates a chunk of it's memory.
-
Close the isloate connection & cancel them once the intended work is completed.
-
When working with Streams, cancel them properly once the task is completed, Otherwise it might lead to memory leak impacting the performance of the app.
-
Dispose all sort of controller in
dispose()
life cycle method. -
Avoid using constructors with a concrete List of children (such as Column() or ListView()) if most of the children are not visible on screen to avoid the build cost.
For more information, Refer the following links:
- Performance considerations in StatefulWidget
- Youtube video on why widgets with const are more performant than functions. Watch here
- More on Opacity
- Understand on Layout Constrints
- Why 60fps
- Performance optimizations of AnimatedBuilder
- Child elements lifecycle and how to load them efficiently, in the ListView.