Flutter UXを支えるパフォーマンス向上Tips

Flutter UXを支えるパフォーマンス向上Tips

Anycloudの青木です。

プロダクト(Flutterアプリ)が成長し、機能が増えるにつれ、どんどん大きくなるコードベース。気づけば動作が重くなり、ユーザー体験が損なわれていく...。そんな問題に直面したくありませんよね…?

今回は、Flutter公式ドキュメントにあるPerformance best practicesをもとに、パフォーマンス向上に役立つTipsをまとめてみました!

16ミリ秒でフレームを構築して表示せよ


フレームを描画するには、ビルドとレンダリングの2つの独立したスレッドがある

そして60fpsのデバイスでは、およそ16ミリ秒ごとで更新する

なぜ、フレームをできるだけ速くビルドしてレンダリングすることを目指すのか?

  • Jankが発生する
    • Jankとは、画面の動き(アニメーションやスクロール)がカクついたり、一瞬止まったりしてスムーズに見えない現象です。画面の描画がフレーム落ちしている状態と言えます。
  • バッテリーの寿命や発熱の問題が発生する

⭐️コラム

  • なぜ60fpsなのか
  • どう検証する?
    • ターゲットとする最も低スペックのデバイスでのパフォーマンスを考慮する
    • Flutter mode: Profileを使用する
      flutter run --profile
      
  • DevToolにおけるPerfomance view
    Dev Tool Performance view
    • Android StudioにおけるFlutter Performance
      • Show widget rebuild informationを有効にする

build( ) のコスト制御せよ


  • 状態をなるべく必要なWidgetに閉じ込める
    • 頻繁に更新される部分は専用のStatefulWidgetにして、そのWidgetだけを再描画する

  • 無駄なコンテナや余分な階層を削減して、Widget ツリーをシンプル に保つ
    • build メソッドでの Widget の作りすぎはパフォーマンスダウンの原因になる

  • 変わらないサブツリーはキャッシュして使い回す
    • 可能な限りconst widgetsを使う
    • final 変数にWidgetを保持する
      • 例)StatefulWidgetState クラス内で final _cachedWidget = SomeWidget(...); のように初期化しておく

    • サブツリーの一部だけが変わるなら、その部分を別のStatefulWidgetに切り出すことで、変化しない部分をキャッシュする
      • 例)AnimatedBuilderbuilder 関数内でアニメーションに依存しないサブツリーを含めない
        AnimatedBuilder(
          animation: myAnimation,
          child: MyStaticWidget(), // アニメーションに依存しないサブツリー
          builder: (context, child) {
            return Transform.rotate(
              angle: myAnimation.value,
              child: child, // 再ビルドされないサブツリーを再利用
            );
          },
        );
        
        
  • ツリーの深さや種類を頻繁に変えず、できるだけ同じ構造のままでプロパティだけ変える
    • ツリー構造が少しでも変化すると、フレームワークが再ビルドや再レイアウト、再ペイントを行う可能性があるため
      • 例)IgnorePointer をラップし、プロパティを切り替える
        • Bad
          Widget build(BuildContext context) {
            if (someCondition) {
              // 子をそのまま返す
              return child;
            } else {
              // 子を IgnorePointer で包む
              return IgnorePointer(child: child);
            }
          }
          
        • Good
          Widget build(BuildContext context) {
            return IgnorePointer(
              ignoring: someCondition, // true or false を切り替える
              child: child,
            );
          }
          
    • Widget構造を変えなければならないとき は、共通部分を GlobalKey 付きのWidgetで包むと、Flutter はその部分を「同一インスタンス」として扱い、不要な再構築・状態リセットを避けられる
      • KeyedSubtree Widgetは、他のWidgetにキーを割り当てることができない場合に便利である

  • UI を再利用するときはヘルパーメソッドではなく、新しいWidgetクラスとして切り出す
    • Keyやコンストラクタのパラメータを使って、Widgetを識別できるようになるため
    • 変更が一切ないウィジェットであれば、const を使うと最も高速に描画・再利用できる
    • Bad
      Widget buildOptionRow(String title) {
        return Row(
          children: [
            Icon(Icons.star),
            Text(title),
          ],
        );
      }
      
    • Good
      class OptionRow extends StatelessWidget {
        final String title;
        const OptionRow({Key? key, required this.title}) : super(key: key);
      
        @override
        Widget build(BuildContext context) {
          return Row(
            children: [
              const Icon(Icons.star),
              Text(title),
            ],
          );
        }
      }
      
      
    • https://youtu.be/IOyq-eTRhvo

参考にした公式ドキュメント

saveLayer()は慎重に使え


UIに様々な視覚効果を実装するために、高価な操作であるsaveLayer()を使うものがある

ただし、saveLayer() の過剰な呼び出しはJankの原因になる

なぜ、saveLayer()は高価なのか?

  • saveLayer() を呼び出すと、オフスクリーンバッファ(裏側の作業用スペース) が作られる
  • そこに描画した内容を最後に画面(オンスクリーン)に合成するため、GPU は「メイン画面 → オフスクリーンバッファ → 再びメイン画面」 と描画先を切り替える
  • 特にスマホのようなモバイルGPU は、描画先を切り替えるとパフォーマンスが落ちやすい
  • できるだけ画面に直接描画して切り替え回数を減らすと、アプリが軽く動きやすい

検証方法

  • DevToolsにおけるPerformanceOverlayLayer.checkerboardOffscreenLayersをチェックする
  • FlutterにおけるMaterialAppのcheckerboardOffscreenLayersをtrueにする
    MaterialApp(
        checkerboardOffscreenLayers: true,
        ...
    )
    

⚠️要注意

明示的にsaveLayer()を呼び出していなくても、使っている他のWidgetやパッケージが裏でsaveLayer()を呼び出している可能性がある

  • 内部でsaveLayer()を使用しているWidgets

OpacityClippingの使用は最小限にせよ


OpacityClippingも同様に高価な操作である

Opacity代用Widget

  • 単純に、0.0 - 1.0の間の不透明度を合成する画像や色が一つだけの場合
    • Color.withOpacity, Image.withOpacity を使う
  • 画像のフェードイン効果を実装したい場合
    • FadeInImage を使う
  • アニメーションで不透明度を変えたい場合
    • AnimatedOpacity , FadeTransition を使う

Clipping代用Widget

  • 多くのWidgetクラスで提供されているborderRadius を使う
    • 例)BoxDecoration

参考にした公式ドキュメント

https://api.flutter.dev/flutter/widgets/Opacity-class.html#performance-considerations-for-opacity-animation

GridsListsを慎重に実装せよ


GridViewListViewは小さなリストではうまく機能するが、多くのアイテムを含むリストを扱うには、GridView.builderListView.builderを使用する

標準のGridViewListViewでは、すべてのアイテムを一度に作成する必要があるのに対し、GridView.builderListView.builderでは、画面にスクロールされるたびにアイテムを作成する

まとめ


アプリのパフォーマンスを意識して、不要な再ビルドや高価な描画処理を避けることが重要で、フレームワークの仕組みを理解しつつ、Widgetの構成や再利用方法を工夫するだけで大きく変わるのでぜひ実践してみてください!

そしてその結果、パフォーマンスが向上し、ユーザー体験につなげていきましょう!

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

青木 陸

PdM

青木 陸

Anycloudでプロダクトマネージャーをしています。