Flutter DevTools プロファイリング実践:メモリリーク追跡とCPUフレームドロップの解決

Flutter DevTools プロファイリング実践:メモリリー크追跡とCPUフレームドロップの解決
Flutterアプリケーションの規模が大きくなると、リストのスクロール時にフレームドロップ(Jank)が発生したり、画面遷移を繰り返す中でメモリ使用量が増加しアプリが強制終了(OOM)するといった問題に直면することがあります。
勘に頼ってパフォーマンスのボトルネックを修正しようとすると、多大な時間が消費されます。Flutter DevToolsは、ヒープダンプ解析、CPUプロファイラ、タイムライン測定を通じて、カクつきやメモリリークの原因を正確に突き止めることができる強力なツールセットです。
本記事では、Flutter DevToolsを活用したCPUフレームドロップの要因分析とメモリリーク(Memory Leak)の追跡手法を実践的なコード例とともに解説します。
1. CPUフレームドロップ(Jank)の追跡と Performance View
Flutterは1秒間に60回(または120回)画面を再描画します。16.6ms(または8.3ms)以内にフレームの構築とレンダリングが完了しない場合、Jank(フレームドロップ)が発生します。
DevTools Timeline の分析ステップ
- アプリを
profileモードで実行します:flutter run --profile - DevToolsを開き、Performance タブへ移動します。
- 赤色で表示されたタイムラインバー(予算時間を超えたフレーム)をクリックします。
- UI Thread と Raster Thread のどちらで遅延が発生しているか確認します。
| スレッド区分 | 主な遅延原因 | 解決策 |
|---|---|---|
| UI Thread | build() メソッド内での重いCPU演算や過剰なオブジェクト生成 |
Isolate.run() への分離、const コンストラクタの活用、Widget再描画範囲の縮小 |
| Raster Thread | 過剰な saveLayer、不要なクリッピング、複雑なシェーダー描画 |
RepaintBoundary の追加、Opacity の代わりに Color.withValues() を使用 |
実践コード:UIスレッドのボトルネック解消
以下はスクロール時にUIスレッドを占유してフレームドロップを引き起こす悪い例と、その改善策です。
// ❌ BAD: build() およびスクロール時に重い演算を実行 (UI Thread Blocking)
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// ビルド中に複雑なデータパースやソートを実行
final processedData = heavyComputation(items[index]);
return Text(processedData);
},
);
}
// ✅ GOOD: 演算を Isolate へオフロードしてキャッシュ化
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return FutureBuilder<String>(
future: Isolate.run(() => heavyComputation(items[index])),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text(snapshot.data!);
},
);
},
);
}
2. メモリリーク診断と DevTools Memory View
メモリリークとは、不要になったオブジェクトがガーベジコレクタ(GC)によって回収されず、ヒープメモリ上に残り続ける現象です。
メモリリークの代表的パターン3選
- Unsubscribed Stream/AnimationController: Stateが
disposeされても購読が生存している場合 - Global/Static Reference: シングルトンや静的変数が
BuildContextやStateを参照し続けている場合 - Closure Capturing: 非同期コールバック内で破棄されるべき
Stateオブジェクトをキャプチャしている場合
DevTools Memory Snapshot による追跡手法
DevToolsの Memory タブにある Snapshot 機能を活用すると、リークしているオブジェクトを視覚的に特定できます。
- Snapshot 1 の撮影: 該当画面に遷移する前のメモリ状態を記録
- 画面遷移の反復: 該当画面を5〜10回開閉する
- GCの実行: DevToolsの「GC」ボタンを押して不要オブジェクトを回収
- Snapshot 2 の撮影: Snapshot 1 との Diff を比較
- 破棄されたはずの画面の
Stateオブジェクトがヒープに残っていないか Retention Path を確認
3. 実践メモリリーク修正パターン
パターン1:StreamSubscription & AnimationController のリーク
// ❌ BAD: disposeでのStreamおよびControllerの破棄漏れ
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late StreamSubscription _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_subscription = eventBus.stream.listen((event) {
setState(() {});
});
}
// dispose() が実装されていないためメモリリークが発生!
}
// ✅ GOOD: dispose() による適切なリソース解放
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late StreamSubscription _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_subscription = eventBus.stream.listen((event) {
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
super.dispose();
}
}
まとめ
Flutterアプリの最適化は、推測ではなく計測データに基づいて行う必要があります。
[!NOTE]
- フレームドロップの診断は必ず
flutter run --profileモードで実施してください。- UIスレッドの負荷は
Isolate.run()で分離し、Rasterスレッドの負荷はRepaintBoundaryや描画処理の単純化で解決します。- DevTools Memory Snapshot の Diff 機能を使い、画面離脱後も残留する
Stateの Retention Path をチェックしましょう。
Flutter DevTools を開発およびCIプロセスに組み込むことで、常に60fpsの滑らかなユーザー体験を維持できます。