Flutter UXを支えるパフォーマンス向上Tips
![Flutter UXを支えるパフォーマンス向上Tips](https://images.microcms-assets.io/assets/4458de1bcd404d52bbf31253395cc937/704e60275113429f92cd572a940693ae/blog_1.png?fm=webp&fit=max&w=3840)
Anycloudの青木です。
プロダクト(Flutterアプリ)が成長し、機能が増えるにつれ、どんどん大きくなるコードベース。気づけば動作が重くなり、ユーザー体験が損なわれていく...。そんな問題に直面したくありませんよね…?
今回は、Flutter公式ドキュメントにあるPerformance best practicesをもとに、パフォーマンス向上に役立つTipsをまとめてみました!
16ミリ秒でフレームを構築して表示せよ
フレームを描画するには、ビルドとレンダリングの2つの独立したスレッドがある
そして60fpsのデバイスでは、およそ16ミリ秒ごとで更新する
なぜ、フレームをできるだけ速くビルドしてレンダリングすることを目指すのか?
- Jankが発生する
- Jankとは、画面の動き(アニメーションやスクロール)がカクついたり、一瞬止まったりしてスムーズに見えない現象です。画面の描画がフレーム落ちしている状態と言えます。
- バッテリーの寿命や発熱の問題が発生する
⭐️コラム
- なぜ60fpsなのか
- どう検証する?
- ターゲットとする最も低スペックのデバイスでのパフォーマンスを考慮する
- Flutter mode: Profileを使用する
flutter run --profile
- DevToolにおけるPerfomance view
- Android StudioにおけるFlutter Performance
- Show widget rebuild informationを有効にする
build( )
のコスト制御せよ
- 状態をなるべく必要なWidgetに閉じ込める
- 頻繁に更新される部分は専用のStatefulWidgetにして、そのWidgetだけを再描画する
- 無駄なコンテナや余分な階層を削減して、Widget ツリーをシンプル に保つ
build
メソッドでの Widget の作りすぎはパフォーマンスダウンの原因になる
- 変わらないサブツリーはキャッシュして使い回す
- 可能な限りconst widgetsを使う
- final 変数にWidgetを保持する
- 例)
StatefulWidget
のState
クラス内でfinal _cachedWidget = SomeWidget(...);
のように初期化しておく
- 例)
- サブツリーの一部だけが変わるなら、その部分を別のStatefulWidgetに切り出すことで、変化しない部分をキャッシュする
- 例)
AnimatedBuilder
のbuilder
関数内でアニメーションに依存しないサブツリーを含めない- 毎フレームで再ビルドされるため、アニメーションに依存しない部分を一度だけビルドし、
AnimatedBuilder
のchild
パラメータとして渡すことで、再ビルドを防ぎ、効率的なアニメーションを実現できる - https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html#performance-optimizations
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, ); }
- Bad
- 例)
- 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- ShaderMask
- ColorFilter
- Chip(
disabledColorAlpha != 0xff
の場合) - Text(
overflowShader
を使用している場合)
Opacity
とClipping
の使用は最小限にせよ
Opacity
とClipping
も同様に高価な操作である
Opacity代用Widget
- 単純に、0.0 - 1.0の間の不透明度を合成する画像や色が一つだけの場合
Color.withOpacity
,Image.withOpacity
を使う
- 画像のフェードイン効果を実装したい場合
FadeInImage
を使う
- アニメーションで不透明度を変えたい場合
AnimatedOpacity
,FadeTransition
を使う
Clipping代用Widget
- 多くのWidgetクラスで提供されている
borderRadius
を使う- 例)BoxDecoration
参考にした公式ドキュメント
Grids
やLists
を慎重に実装せよ
GridView
やListView
は小さなリストではうまく機能するが、多くのアイテムを含むリストを扱うには、GridView.builder
やListView.builder
を使用する
標準のGridView
やListView
では、すべてのアイテムを一度に作成する必要があるのに対し、GridView.builder
やListView.builder
では、画面にスクロールされるたびにアイテムを作成する
まとめ
アプリのパフォーマンスを意識して、不要な再ビルドや高価な描画処理を避けることが重要で、フレームワークの仕組みを理解しつつ、Widgetの構成や再利用方法を工夫するだけで大きく変わるのでぜひ実践してみてください!
そしてその結果、パフォーマンスが向上し、ユーザー体験につなげていきましょう!