effidevFlutter・Cloudflareエッジ・クラウドコスト最適化
日本語

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

Flutter DevTools Profiling and Performance Optimization

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 の分析ステップ

  1. アプリを profile モードで実行します:
    flutter run --profile
  2. DevToolsを開き、Performance タブへ移動します。
  3. 赤色で表示されたタイムラインバー(予算時間を超えたフレーム)をクリックします。
  4. UI ThreadRaster 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選

  1. Unsubscribed Stream/AnimationController: Stateが dispose されても購読が生存している場合
  2. Global/Static Reference: シングルトンや静的変数が BuildContextState を参照し続けている場合
  3. Closure Capturing: 非同期コールバック内で破棄されるべき State オブジェクトをキャプチャしている場合

DevTools Memory Snapshot による追跡手法

DevToolsの Memory タブにある Snapshot 機能を活用すると、リークしているオブジェクトを視覚的に特定できます。

  1. Snapshot 1 の撮影: 該当画面に遷移する前のメモリ状態を記録
  2. 画面遷移の反復: 該当画面を5〜10回開閉する
  3. GCの実行: DevToolsの「GC」ボタンを押して不要オブジェクトを回収
  4. Snapshot 2 の撮影: Snapshot 1 との Diff を比較
  5. 破棄されたはずの画面の 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]

  1. フレームドロップの診断は必ず flutter run --profile モードで実施してください。
  2. UIスレッドの負荷は Isolate.run() で分離し、Rasterスレッドの負荷は RepaintBoundary や描画処理の単純化で解決します。
  3. DevTools Memory Snapshot の Diff 機能を使い、画面離脱後も残留する State の Retention Path をチェックしましょう。

Flutter DevTools を開発およびCIプロセスに組み込むことで、常に60fpsの滑らかなユーザー体験を維持できます。