【Flutter】Flutterのパフォーマンスの考慮
はじめに
皆さん、こんにちは。
今回はFlutterのパフォーマンス最適化の原理原則についてご紹介します。
参考
パフォーマンスを高める基本的な考え方
要点
- パフォーマンスの課題の多くは、ウィジェットの不要な再構築から発生
-
buildメソッドの実行コストを最小限に抑える- 高コストな処理が含まれるとツリーの生成に時間がかかる
-
setState()の不適切な使用による不必要なツリーの再構築を避ける - 複雑なウィジェットの多用するとツリーの計算コストを増大させる
- 「最小限の更新と再構築」が基本
Flutterでアプリを開発する際、ユーザーにサクサクと快適に使ってもらうためには、「いかに無駄な処理を減らすか」が非常に重要になります。特に、画面の表示に関わる「ウィジェットの再構築」のコストをコントロールすることが、パフォーマンス向上の鍵となります。
パフォーマンスの課題の多くは、ウィジェットの不要な再構築から発生
アプリの画面は、ウィジェットの組み合わせでできています。ユーザーがボタンを押すなどして画面の一部が更新されると、ウィジェットツリーの一部を「再構築(リビルド)」し、新しい部品で画面を書き換えます。
この「再構築」のたびに、新しいウィジェットのインスタンスが次々と生成されます。もし、見た目や設定が全く変わらないウィジェットまで毎回新しく作っていたら、それは無駄なインスタンス生成となり、アプリの動作を重くする原因になってしまいます。
build メソッドの実行コストを最小限に抑える
ウィジェットの再構築は、それぞれのウィジェット内部にあるbuildメソッドが実行されることで起こります。このbuildメソッドは再構築のたびに呼ばれるため、その中身の処理コストを最小限に抑えることが、パフォーマンス向上の基本中の基本となります。
もし、このbuildメソッド内に時間のかかる高コストな処理(例えば、複雑なデータ変換や大量の計算など)が含まれていると、画面が更新されるたびにツリーの生成に余計な時間がかかってしまい、アプリの表示が遅れる原因となります。
setState()の不適切な使用による不必要なツリーの再構築を避ける
setState()が呼ばれると、そのウィジェットとその子孫(下層の)ウィジェット全体が再構築の対象になってしまいます。
例えるなら、家の壁の一部を塗り直したいだけなのに、家全体を建て直そうとするようなものです。
本当に小さな部分だけを更新したいのに、巨大なウィジェットツリー全体を何度も再構築してしまうと、アプリの動作は当然ながら重くなります。
複雑なウィジェットの多用するとツリーの計算コストを増大させる
アプリの見た目を良くするために、一つのウィジェットの中に非常に多くの部品を詰め込み、複雑なウィジェットを構築してしまうと、パフォーマンスの観点から問題が生じることがあります。
複雑なウィジェットを使うと、一度の再構築でFlutterが処理しなければならない情報量が増え、ツリーの計算コストを増大させてしまいます。再構築のたびに、たくさんのウィジェットを比較・計算する必要が生じるため、その分時間がかかってしまうのです。
「最小限の更新と再構築」が基本
ここまでの課題を解決するための結論はシンプルです。パフォーマンス向上は、「最小限の更新と再構築」を徹底することから始まります。
-
必要な部分だけを再構築する:
setState()の影響範囲を小さく保つ。 -
buildメソッドを軽くする:複雑な計算や重い処理をbuildメソッドの外に出す。 - ウィジェットを小さく分ける:巨大なウィジェットツリーを避け、再利用可能で独立した小さなウィジェットに分割する。
これらの原則を守ることで、Flutterは「変更されていない部品の作業をスキップ」できるようになり、アプリは本当に必要な処理だけに集中できるようになります。
const コンストラクタを活用する
要点
-
constコンストラクタはで生成したインスタンスは再利用される -
constコンストラクタで生成したインスタンスはツリーの再構築をスキップされる- 無駄なインスタンス生成を避けられる
- 自作のウイジェットでプロパティが全て不変(
final)ならconstコンストラクタを用意する - 標準のウィジェットならば
constコンストラクタが使えるウィジェットを選択する- 例えば背景色を指定するためにContainerを使うより、ColoredBoxを使う。(constantコンストラクタを持ってる代替可能なウィジェット)
「最小限の更新」を実現するための、最も強力かつ手軽な手段、すなわち**constコンストラクタを活用する方法**について深く掘り下げます。
constコンストラクタはで生成したインスタンスは再利用される
constコンストラクタは、Flutterに「このウィジェットは不変(絶対に変わらない)」という保証を与えるキーワードです。
constキーワードを付けて作られたウィジェット(インスタンス)は、プログラムの実行中に一度だけ生成されます。
例えるなら、一度だけ設計図通りに作られた「標準部品」として、アプリ内の特別な「貯蔵庫」に大切に保管されるようなものです。
アプリの画面が何度更新され、同じ部品が再び必要になっても、Flutterは新しく部品を作るのではなく、この貯蔵庫から既存の部品を再利用します。
これにより、ウィジェットの無駄なインスタンス生成というCPUへの負担を大きく減らすことができます。
const コンストラクタで生成したインスタンスはツリーの再構築をスキップされる
最も強力なメリットはこれです。親ウィジェットが状態を変更して再構築されても、その子にあるconstウィジェットは、自分自身の再構築作業を完全にスキップされます。
Flutterは「この部品はconstだから、中身が変わらないことが保証されている。手間をかけて作り直す必要はない」と判断します。この賢い判断のおかげで、ウィジェットツリーの中で変更されていない部分に対する計算コストをゼロにでき、アプリの描画速度が向上します。
自作のウイジェットでプロパティが全て不変(final)ならconstコンストラクタを用意する
自分で作ったウィジェットにconstコンストラクタを導入することは、パフォーマンス改善の最初の一歩であり、最も大切な習慣です。
もし、あなたの作ったウィジェットのプロパティ(大きさ、色、表示するテキストなど、そのウィジェットが持つデータ)がすべてfinal(不変、つまり一度設定したら二度と変更されない)であるならば、必ずそのウィジェットのコンストラクタにconstを付けましょう。
通常のコンストラクタのみの例
// Bad: パフォーマンス最適化の機会を逃している
class MyTextWidget extends StatelessWidget {
final String message;
// const がない
MyTextWidget({required this.message});
// ...
}
constコンストラクタがある例(既存のコンストラクタにconstを付与)
// Good: const を追加することで、再利用と再構築スキップの対象になる
class MyTextWidget extends StatelessWidget {
final String message;
// const を追加!
const MyTextWidget({required this.message});
// ...
}
標準のウィジェットならばconstコンストラクタが使えるウィジェットを選択する
Flutterが標準で提供しているウィジェットを使う際にも、常に「constコンストラクタが使えるか?」という視点を持つことが重要です。
たとえば、画面に背景色を指定したい場合を考えてみましょう。
- 多機能なContainerウィジェット:背景色指定もできますが、他にも多くの機能(マージン、パディング、装飾など)を持つため、シンプルに使う場合は少しだけオーバーヘッド(余分な処理)が発生しやすいです。
-
シンプルなColoredBoxウィジェット:これは、単に「色付きの箱」を表示することだけに特化しており、
constコンストラクタを持っています。
もし背景色を指定するだけなら、Containerを使うよりも、const ColoredBox(...)を使う方が、シンプルかつパフォーマンスに優れています。
このように、目的を果たすために、よりシンプルでconstコンストラクタが使える代替可能なウィジェットを選ぶ習慣を持つことが、アプリ全体の快適さに繋がります。
公式ドキュメントでの言及
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 use const when possible, enable the recommended lints from the flutter_lints package. For more information, check out the flutter_lints migration guide.
可能な限りウィジェットに const コンストラクタを使用してください。これにより Flutter は再構築作業の大部分をショートカットできます。const の使用を自動的にリマインドするには、flutter_lints パッケージの推奨される lint を有効にしてください。詳細は flutter_lints 移行ガイドを参照してください。
Use const widgets where possible. (This is equivalent to caching a widget and re-using it.)
可能な限りconstウィジェットを使用してください。(これはウィジェットをキャッシュして再利用することと同等です。)
build メソッドを軽量に保つ
要点
-
buildは頻繁に呼ばれるので重たい処理を持たせない - ロジックは
initState()で呼び出すなどタイミングをずらす - Riverpodなどの状態管理ツールにロジックを分離する
-
buildには加工済みのデータを渡し、buildは表示に特化する -
IsolateやFutureを使って非同期化する - 重い処理の例
- ネットワークアクセス
- 大規模Listの生成と即時評価(
list.where(...).toList()) - その他重い計算…etc
buildは頻繁に呼ばれるので重たい処理を持たせない
ウィジェットのbuildメソッドは、頻繁に呼ばれます。画面の初期表示時だけでなく、ユーザーがボタンを押したとき、タイマーが動いたとき、アニメーションの最中など、状態が変わるたびに実行されます。
buildメソッドの役割は「表示に関することだけ」に特化させ、重たい処理は一切持たせないことが鉄則です。
では、buildメソッドが軽快に動くように、重い処理をどこへ移動させればよいのでしょうか?重い処理を分離する4つのテクニックを紹介します。
ロジックはinitState() で呼び出すなどタイミングをずらす
ウィジェットの初期化や、一度だけ実行すれば良い処理(例:初期データの読み込み)は、buildメソッドの実行タイミングからずらす必要があります。
StatefulWidget(状態を持つウィジェット)であれば、ウィジェットが最初に作られるときだけ実行されるinitState()(イニットステート)メソッドで呼び出すのが適切です。こうすることで、再構築のたびに同じ処理が繰り返されることを防げます。
Riverpodなどの状態管理ツールにロジックを分離する
アプリのビジネスロジック(例:データ取得、計算、状態の保持)は、画面の描画とは分離すべきです。RiverpodやProviderなどの状態管理ツールを使うことで、重たい処理をウィジェットの外にある専用の保管庫(ロジック層)に移動できます。
buildには加工済みのデータを渡し、buildは表示に特化する
上記により、buildメソッドにはすでに加工済みのデータが渡されるようになり、buildはそのデータを表示することだけに集中できます。キッチン(build)には、すぐに盛り付けられる完成品(加工済みデータ)だけを渡すイメージです。
IsolateやFutureを使って非同期化する
特に時間のかかる大規模な計算処理(大量のデータ処理など)は、アプリの動きを止めてしまう可能性があります。これを避けるため、**Isolate**やFutureといった仕組みを使って非同期化しましょう。
-
Future: 時間はかかるがメインの処理を止めずに待つ仕組み。(例:ネットワークからのデータ取得) -
Isolate: メインの処理とは完全に独立した別の場所で計算を実行する仕組み。これを使うと、非常に重い計算でもアプリの動きを止めることなく並行して処理できます。
重い処理の例
具体的に、buildメソッド内に置いてはいけない処理には、以下のようなものがあります。
- ネットワークアクセス: サーバーからデータを取得する処理。
-
大規模リストの即時評価: 例えば、大きなリストから特定の条件でデータを抽出し、新しいリストをすぐに作る
list.where(...).toList()などの処理。 - 複雑で時間のかかる数学的な計算。
これらの処理はすべて、buildメソッドから追い出し、ロジック層や非同期処理として実行すべきです。
公式ドキュメントの言及
Avoid repetitive and costly work in build() methods since build() can be invoked frequently when ancestor widgets rebuild.
ビルド()メソッドでは、繰り返し発生する高コストな処理を避けるべきです。祖先ウィジェットの再構築時にビルド()が頻繁に呼び出される可能性があるためです。
Minimize the number of nodes transitively created by the build method and any widgets it creates.
buildメソッドとそのメソッドが作成するウィジェットによって、推移的に(連鎖的に)作成されるノードの数を最小限に抑えること。
効率的なウィジェットを選択
要点
- 複数のレイアウトウィジェットを重ねるとパフォーマンスに影響
- レイアウトの計算量が増える
- インスタンス数が増えメモリ消費やツリー再構築のコストが増える
- ネストせず1つで賄えるウィジェットを利用する
-
AlignやCustomSingleChildLayoutなど
-
画面の見た目を実現するために、どのようなウィジェットを選ぶか、またどのように組み合わせるかという選択が、実はアプリのパフォーマンスに大きく影響します。
複数のレイアウトウィジェットを重ねるとパフォーマンスに影響
必要以上に多くのレイアウトウィジェットを重ねてしまうと、パフォーマンスに影響が出ます。
-
レイアウトの計算量が増える
- Flutterは、画面にウィジェットを配置する際、親ウィジェットから子ウィジェットへと「ここはどれくらいの大きさで、どこに配置できるか」という指示を伝え、子ウィジェットは「自分はどれくらいの大きさが必要か」という情報を親に返すという、計測と配置のプロセスを繰り返しています。
- 複数のレイアウトウィジェットを深くネスト(入れ子)にすると、このレイアウトの計算を何度も何度も繰り返す必要が出てきます。ウィジェットの数が増えるほど、この計算にかかる時間が増大し、特に複雑な画面では、描画が遅れる原因となります。
-
インスタンス生成とメモリ消費が増加する
- ウィジェットを重ねるということは、それだけウィジェットのインスタンス(部品そのもの)の数が増えるということです。インスタンスが増えれば、その分メモリを消費し、再構築が必要になった際のツリー再構築のコストも増えてしまいます。
ネストせず1つで賄えるウィジェットを利用する
パフォーマンスを向上させるためには、一つの目的を達成するために、できるだけ少ないウィジェットで実現することが理想です。つまり、ネスト(入れ子構造)を避けて、単一で多機能なウィジェットを利用することが重要になります。
例えば、単純に子ウィジェットを親ウィジェットの中央に配置したいだけの場合を考えてみましょう。
-
非効率な例: まず
Containerを置いて、その中にPaddingを入れ、さらにその中にCenterを入れる…のように複数のウィジェットを重ねてしまう。 -
効率的な例: 単純に
Centerウィジェットだけを使う。
さらに高度なレイアウトを実現したい場合は、以下のようなウィジェットが選択肢になります。これらは、複数のウィジェットの機能を一つにまとめて、効率的にレイアウトを計算できるように設計されています。
-
Align: 子ウィジェットを親の特定の位置(左上、中央下など)に配置する機能に特化しています。 -
CustomSingleChildLayout: 一つの子ウィジェットに対して、非常に細かく独自の配置ルールを適用したい場合に便利です。
ウィジェットを多用する前に、「本当にこのウィジェットが必要か?」「これ一つで賄える、より効率的なウィジェットはないか?」と立ち止まって考える習慣が大切です。
公式ドキュメントの言及
Instead of an elaborate arrangement of Rows, Columns, Paddings, and SizedBoxes to position a single child in a particularly fancy manner, consider using just an Align or a CustomSingleChildLayout.
一つの子ウィジェットを特に凝った方法で配置するために、Row、Column、Padding、SizedBoxを精巧に組み合わせるのではなく、AlignやCustomSingleChildLayoutだけを使用することを検討してください。
状態の参照はツリーの末端で行う
要点
- 状態を参照するウィジェットは末端に分離させる
- 状態の変更によるツリーの再構築範囲を最小限にできる
- 巨大なウィジェット(Massive Widget)は、小さな状態変化でも広い範囲を再構築してしまう原因となる
- Riverpodの状態監視は末端のウィジェットで行う
- 状態を扱うウィジェットを末端に切り出すのと同じ
- Riverpodの状態監視をしているウィジェットのツリーを大きくしすぎず、最小限のサイズに切り出す
状態を扱う際に重要なことは、「状態の変化が、アプリのどこまで影響を及ぼすか」をコントロールすることです。このコントロールを誤ると、小さなデータ変更一つで画面全体が再構築され、アプリが遅くなる原因となります。
状態を参照するウィジェットは末端に分離させる
Flutterアプリにおいて、ユーザーの操作やサーバーからのデータによって状態(データ)が変更されると、そのデータを使っているウィジェットとその子孫のウィジェットが再構築(作り直し)の対象になります。
一つのファイル内に、画面の大部分を占める巨大なウィジェットを作ってしまうと、その中でごく小さな状態の変化(例:いいね!ボタンの数字が1つ増える)があっただけで、巨大なウィジェット全体が再構築されてしまいます。これが、「もたつき」を生む大きな原因の一つです。
これを避けるため、ウィジェットを可能な限り小さく、特定の機能に特化させて切り出すことが非常に重要です。
状態の変更によるツリーの再構築範囲を最小限にでき
再構築の範囲を最小限にします。この原則の目的は、状態の変更によるツリーの再構築範囲を最小限に抑えることです。
例えるなら、巨大なショッピングモール(ウィジェットツリー)の中央にある巨大な柱(親ウィジェット)が、すべての店舗(子孫ウィジェット)の照明や空調を管理しているとします。
もし柱の電力設定を少し変えるだけで、モール全体の照明が一度消えて、また点け直されるとしたら、非常に非効率です。
これを避けるには、状態(電力)を扱うウィジェットを、本当にそのデータが必要な場所(例:特定の店舗のレジ周りだけ)の末端に分離させる必要があります。こうすることで、レジのデータが変わっても、再構築されるのはそのレジウィジェットのごく小さな範囲だけで済むのです。
Riverpodの状態監視は末端のウィジェットで行う
Flutter開発では、Riverpodのような状態管理ツールを使って状態を扱います。これらのツールを使う際も、「状態の参照は末端で」という原則は変わりません。
Riverpodでは、ref.watch()などの命令を使ってウィジェット内で状態(データ)を監視し、データが変更されたときに自動的に再構築が行われます。
この状態監視をしているウィジェットのツリーを大きくしすぎず、最小限のサイズに切り出すことが、Riverpodを使う上でのパフォーマンス最適化の鍵となります。
- 悪い例: 画面全体を覆うウィジェットですべてのデータを監視し、そのウィジェットに大量の子ウィジェットを持たせてしまう。
-
良い例: 画面全体は監視せず、いいね!ボタンの数字を表示する部分など、データが直接影響する末端のウィジェットだけで、必要なデータだけを監視(
watch)する。
これは、状態を扱うウィジェットを末端に切り出すという、先の原則を状態管理ツールにも適用した方法です。
公式ドキュメントの言及
When setState() is called on a State object, all descendent widgets rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree.
StateオブジェクトでsetState()が呼び出されると、すべての子孫ウィジェットが再構築されます。したがって、setState()の呼び出しは、UIの変更が実際に必要なサブツリーの部分に限定してください。変更がツリーのごく一部に限定されている場合、ツリーの上位でsetState()を呼び出すことは避けてください。
If the widget is likely to get rebuilt frequently due to the use of InheritedWidgets, consider refactoring the stateless widget into multiple widgets, with the parts of the tree that change being pushed to the leaves.
InheritedWidgetの使用によりウィジェットが頻繁に再構築される可能性が高い場合、ステートレスウィジェットを複数のウィジェットにリファクタリングし、ツリーの中で変化する部分を末端(葉)に移動させることを検討してください。
Push the state to the leaves. For example, if your page has a ticking clock, rather than putting the state at the top of the page and rebuilding the entire page each time the clock ticks, create a dedicated clock widget that only updates itself.
状態をリーフにプッシュする。例えば、ページに時計が表示されている場合、状態をページ最上部に配置して時計が刻むたびにページ全体を再構築するのではなく、自身のみを更新する専用の時計ウィジェットを作成する。
パフォーマンス計測ツール
別途ツールとして計測方法があります。
おわりに
これまでに学んだ重要な原則を振り返り、明日からのコードレビューに使える実践チェックリストとしてまとめます。
| 分野 | 原則 | 具体的なアクション |
|---|---|---|
| 静的コストの削減 | constコンストラクタを活用する |
🔸 不変のウィジェット(プロパティがfinalのみ)には必ずconstを用意する。 |
🔸 既存ウィジェットもconstが使えるシンプルな代替品(例:ContainerではなくColoredBox)を選ぶ。 |
||
| 動的コストの削減 | buildメソッドを軽量に保つ |
🔹 buildメソッド内に重たい処理(大規模な計算、ネットワークアクセスなど)は絶対に書かない。 |
🔹 ロジックはinitState()や状態管理ツール(Riverpodなど)に分離し、buildには加工済みのデータだけを渡す。 |
||
| レイアウトの効率化 | 効率的なウィジェットを選択する | 📐 複数のレイアウトウィジェット(Padding、Alignなど)を不必要に重ねるのを避ける。 |
📐 単一で目的を達成できるウィジェット(例:Align)を積極的に利用する。 |
||
| 再構築範囲の制御 | 状態の参照はツリーの末端で行う | 🎯 状態の監視(ref.watchなど)やsetStateの影響範囲を絞るため、ウィジェットを小さく切り出す。 |
| 🎯 巨大なウィジェット(Massive Widget)を作らず、状態を参照するウィジェットを必要な場所の末端に配置する。 |
Discussion