flutter-performance について
flutter-performanceは、Flutter開発者向けのパフォーマンス改善スキルです。実機の低性能デバイスでプロファイルモード測定を行い、再ビルドや描画の負荷、UIスレッドの処理を見直す方法を示します。
主な機能
- 01状態変化に応じて再ビルド範囲を限定
- 02長いリストを遅延ビルド
- 03重い処理をbuild()の外へ移動
- 04表示サイズに合わせて画像をデコード
- 05DevToolsでUIとラスタースレッドを計測
- 06リソース解放と描画負荷を管理
活用シーン
- 01低性能デバイスでのフレーム落ちを調査
- 02長いリストのビルド負荷を軽減
- 03アニメーションや画像による描画負荷を診断
- 04プロファイルモードで変更前後の性能を比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill flutter-performanceインストール後、Claude Code で/flutter-performanceを入力して読み込まれたか確認します。
詳細
Flutter パフォーマンス
低スペック端末(ローエンドの Android/旧型 iPhone)で、安定して 60 fps(1 フレームあたり 16 ms 以下、120 Hz では 8 ms 以下)を維持します。パフォーマンスはプロファイルモードで測定するものであり、主張するものではありません。再ビルドを減らし、描画コストを抑え、UI スレッドとラスタースレッドの両方を空けておくことで実現できます。
絶対に守るルール
- 可能なものはすべて
constにします。constサブツリーは再ビルド時にスキップされます。フレームワークが同一性によって処理をショートカットするためです。prefer_const_constructorsはエラーとして有効にしておきます。constにできるのにそうしていないリテラルは、好みの問題ではなく lint エラーです。 - 再ビルドの範囲を、変更される最小のウィジェットまで絞ります。 1 つのフィールドが変わったときに、状態オブジェクト全体を対象に
ref.watch(provider)してはいけません。provider.select((s) => s.field)で監視します。HUD のカウンターの更新で、重い兄弟ウィジェットを再ビルドしてはいけません。再ビルド範囲を絞ることは、ビルドスレッドのコストを下げる最も大きな手段です。 - 長いコンテンツはすべて遅延読み込みにします。 変動するコンテンツや終わりのないコンテンツには、
ListView.builder/GridView.builder/スライバーを使います。ListView(children: items.map(...).toList())は画面外の行もすべて最初にビルドします。ごく小規模で固定されたもの以外には使わないでください。 build()内でコストの高い処理を行いません。jsonDecode、ソート、正規表現、DateTimeの計算、ファイルやネットワークへのアクセス、大規模なメモリ割り当ては避けます。build()は毎フレーム呼び出される可能性があります。Notifier/ViewModel で一度だけ計算し、不変の状態に結果をキャッシュします。- CPU 負荷の高い処理は UI アイソレートの外で行います。 大きなペイロードの解析、画像処理、数値計算には
compute()または生成したIsolateを使います。UI スレッドをブロックすると必ずカクつきが発生します。ラスタースレッドでも防げません。 - 画像のデコードサイズを表示領域に合わせます。
cacheWidth/cacheHeightまたはResizeImage、適切な解像度のアセット、画面の初期表示範囲にある画像へのprecacheImageを使います。4000 px の画像を 100 px のボックスにデコードすると、メモリ使用量が急増し、低価格のスマートフォンでは OOM が発生します。 - 目的を果たせる最も軽量なウィジェットを選びます。
ContainerよりColoredBox/DecoratedBox、余白にはSizedBox、ホットパスではOpacityウィジェットよりFadeTransition/AnimatedOpacityを使います。スクロールリスト内のClipPath/saveLayerは避けてください。ラスタースレッド上にオフスクリーンバッファーが強制的に作られます。 - コストの高い、独立して再描画されるサブツリー(アニメーション、チャート、ライブインジケーターなど)を
RepaintBoundaryで囲みます。 そのサブツリーの再描画によって、隣接する要素まで再ラスタライズされるのを防げます。あちこちに境界を追加しないでください。それぞれがメモリコストのかかるコンポジターのレイヤーになります。 - コストの高い子ウィジェットは
child:経由で渡します。AnimatedBuilder/ValueListenableBuilder/StreamBuilderはbuilderのみを再ビルドします。変更されないサブツリーはchild:経由で渡すことで、フレームごとではなく一度だけビルドされます。 - リークを防ぐため、リソースを破棄します。
AnimationController、ジェスチャー認識器、ストリームのサブスクリプション、タイマー、TextEditingController、キャッシュは、ウィジェットではdispose()、プロバイダーではref.onDisposeで解放します。画面を離れた後も状態が残らないよう、セッションスコープの状態にはautoDisposeを使います。 - 実機の低スペック端末で DevTools を使い、PROFILE モードで測定します。 デバッグ時の計測値には意味がありません(JIT、アサーション)。UI スレッドだけでなくラスタースレッドも確認します。
saveLayer/ぼかし/クリップのコストは Dart ではなく、ラスタースレッドに現れます。測定せずに主張したフレーム予算の数値を、リリース版に持ち込んではいけません。
再ビルド範囲を絞る
状態オブジェクト全体ではなく、変更される単一のフィールドを監視します。
// Rebuilds only when the count changes, not on every OrderState mutation.
class OrderBadge extends ConsumerWidget {
const OrderBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(orderProvider.select((s) => s.items.length));
return Text('$count');
}
}
.select はパブリッシュのたびにセレクターを実行しますが、選択された値の == が変わった場合にのみ再ビルドします。そのため、入力には実際の等価性を持つ不変の値を使う必要があります。watch/read/listen の使い分けや、family と autoDispose については state-management-riverpod を参照してください。
コストの高い子ウィジェットをアニメーションループの外に保つ
// The card is built once; only the transform recomputes each tick.
AnimatedBuilder(
animation: _controller,
child: const ProductCard(),
builder: (_, child) =>
Transform.scale(scale: _controller.value, child: child),
);
遅延読み込みリストとサイズ指定済み画像
// Lazy — only visible rows are built.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(item: items[i]),
);
// Decode to the slot, not the source resolution.
Image.asset('assets/hero.png', cacheWidth: 240);
CPU 負荷の高い処理を UI アイソレートの外で行う
// parseItems is a top-level or static function (isolate entry-point rules).
final items = await compute(parseItems, jsonString);
カスタム描画
CustomPainterでは、paint()でメモリー割り当てを一切行わず、不変のシーン値を1回比較するだけのshouldRepaintを実装し、ティッカー内のsetStateではなく、repaint:にListenable(AnimationController)を指定します。この方法についてはcustom-canvas-and-gesturesで説明しています。ここで押さえておくべきパフォーマンス上の重要なルールは、paint()内でPaint/Path/Gradientを絶対に割り当てないことです。ペインターのフィールドとして保持し、コストの低いプロパティだけを変更してください。
プロファイリングのワークフロー
- 実機で
flutter run --profileを実行します。 - DevTools -> Performanceを開き、カクつきを再現しながら記録します。
- 予算を超えた各フレームについて、所要時間がBuild、Layout、Rasterのどこで発生しているかを確認します。
- Buildが重い場合 ->
.selectで再ビルドの範囲を絞り、constを追加し、build()の外に処理を移します。 - Rasterが重い場合 ->
Opacity/クリップ/saveLayerを減らし、RepaintBoundaryを追加し、シェーダーを簡素化します。
- Buildが重い場合 ->
- 「Track widget rebuilds」を使い、再ビルドが多すぎるウィジェットを見つけます。
- ボトルネックを1つ修正して、再測定します。修正前後を測定せずに最適化してはいけません。
アンチパターン
- デバッグモードでパフォーマンスを判断する — JITとアサーションにより、計測時間は当てになりません。常にプロファイルモードと実機を使ってください。
- プロファイルデータなしで時期尚早な最適化をする — 複雑さが増すだけで、問題は隠せません。まず測定してください。
- ツリーの上位で
setState()を呼び、小さな変更のために画面全体を再ビルドする — 状態の範囲を狭めてください。 - リーフウィジェットでオブジェクト全体に
ref.watch(provider)を使い、変更されたのは1つのフィールドだけ —.selectを使ってください。 - 長いコンテンツや長さが不定のコンテンツに
ListView(children: [...].toList())を使う — 画面外の行もすべてビルドされます。 build()内でjsonDecode/ソート/DateTimeの計算/正規表現を実行する — 毎フレーム実行される可能性があります。ViewModelで事前に計算してください。- スクロールリストのフェードに
Opacityウィジェットを使う —saveLayerが強制されます。FadeTransitionを使うか、色を使ってフェードさせてください。 - 小さなウィジェットにフル解像度の画像をデコードする — メモリー使用量が急増し、低価格帯のスマートフォンではOOMが発生します。
- UIアイソレートで大きなペイロードを解析したり、負荷の高いループを実行する — フレームが停止します。
computeを使ってください。 - あらゆるものに
RepaintBoundaryを付ける — レイヤーのメモリー使用量が膨らみます。個別に再描画されるサブツリーの周囲に必要な場合だけ使ってください。 - 高コストな子ウィジェットを
child:で渡さずに、AnimatedBuilder/StreamBuilderで再ビルドする。 - デバウンスなしでキー入力のたびに再ビルドする、または上限のないキャッシュ/リスナーによるリーク。
paint()内でPaint/Pathを割り当てる — フレームごとにガベージが発生します。フィールドとして保持してください。
完了の定義
- 適用可能な箇所すべてに
constを適用し、prefer_const_constructorsの警告がない。 -
.selectで再ビルドの範囲を限定し、あるウィジェットのティックによって重い兄弟ウィジェットが再ビルドされない。 - 長いリストや可変長のリストが遅延読み込みされる(
.builder/スライバー)。 -
build()内にI/O、解析、負荷の高い計算がなく、大きな処理はcompute/Isolateで実行される。 - 画像のサイズが表示サイズに合わせられ(
cacheWidth/ResizeImage)、ファーストビューの画像が事前キャッシュされる。 - 適切な中で最も低コストなウィジェット/エフェクトを選び、
RepaintBoundaryを必要な箇所だけに使い、高コストな子ウィジェットをchild:で渡す。 - すべての
CustomPainterがpaint()内で何も割り当てず、shouldRepaintが値を1回比較するだけである。 - コントローラー、認識器、サブスクリプション、タイマーが破棄され、スコープ付きセッション状態が
autoDisposeされ、リークがない。 - 実機でプロファイルモードを使いDevTools経由で測定し、UIスレッドとラスタースレッドの両方が予算内で、16 msを超えるフレームがなく、その主張が記録で裏付けられている。
関連するスキル
state-management-riverpod— watch/read/listenの使い分け、.select、familyとautoDispose、.selectが正しく機能する不変の状態。widget-composition—_buildXメソッドよりも小さなconstWidgetクラスを使うこと、破棄の徹底、最も低コストなウィジェットの選択。custom-canvas-and-gestures— メモリー割り当てのないpaint()、値を1回比較するだけのshouldRepaint、repaint:のListenable。testing-strategy— 重い計算をUIアイソレート外でテスト可能にする、クロックを注入した純粋なコア。