Skip to content

Latest commit

 

History

History
181 lines (145 loc) · 9.33 KB

performance.md

File metadata and controls

181 lines (145 loc) · 9.33 KB

Performance Best Practices

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.

Avoid/Minimize Expensive Operations

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.

Using build() thoughtfully

  • 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 call setState() 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
        ],
        );
    }

Avoid/Minimize calls to saveLayer()

  • 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.

    Shapes Overlap

  • 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 if disabledColorAlpha != 0xff
    • Text if there’s an overflowShader
    • Clipping when type is Clip.antiAliasWithSaveLayer
    • ...

Minimize use of opacity and clipping

  • Animating an Opacity widget directly causes the widget (and possibly its subtree) to rebuild each frame, which is not very efficient. Consider using an AnimatedOpacity or a FadeTransition instead.

  • Directly drawing an Image or Color with opacity is faster than using Opacity on top of them because Opacity 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),
        ),
        ...
    )

Working with Grids & Lists

  • 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.

Other Optimizations

  • 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.

Resources

For more information, Refer the following links: