# Flutter UXを支えるパフォーマンス向上Tips

> Flutterアプリのパフォーマンス向上のためのヒントやベストプラクティスを紹介し、ユーザー体験を損なうことなくスムーズな動作を実現するための具体的な方法を解説します。

- 公開日: 2025-01-22
- 著者: 青木 陸
- タグ: Flutter, パフォーマンス
- URL: https://tech.anycloud.co.jp/articles/flutter-performance

---

Anycloudの青木です。

プロダクト（Flutterアプリ）が成長し、機能が増えるにつれ、どんどん大きくなるコードベース。気づけば動作が重くなり、ユーザー体験が損なわれていく...。そんな問題に直面したくありませんよね…？

今回は、Flutter公式ドキュメントにある[Performance best practices](https://docs.flutter.dev/perf/best-practices)をもとに、パフォーマンス向上に役立つTipsをまとめてみました！

## 16ミリ秒でフレームを構築して表示せよ

* * *

フレームを描画するには、ビルドとレンダリングの２つの独立したスレッドがある

そして60fpsのデバイスでは、およそ16ミリ秒ごとで更新する

なぜ、フレームをできるだけ速くビルドしてレンダリングすることを目指すのか？

-   Jankが発生する
    -   Jankとは、画面の動き（アニメーションやスクロール）がカクついたり、一瞬止まったりしてスムーズに見えない現象です。画面の描画がフレーム落ちしている状態と言えます。
-   バッテリーの寿命や発熱の問題が発生する

**⭐️コラム**

-   なぜ60fpsなのか
    -   <iframe class="embed-youtube" src="https://www.youtube.com/embed/CaMTIgxCSqU" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
        
-   どう検証する？
    -   ターゲットとする最も低スペックのデバイスでのパフォーマンスを考慮する
    -   [Flutter mode: Profile](https://docs.flutter.dev/testing/build-modes#profile)を使用する
        
        ```bash
        flutter run --profile
        ```
        
-   DevToolにおけるPerfomance view
    
    -   <div class="link-card-wrap"><a class="link-card" href="https://docs.flutter.dev/tools/devtools/performance" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">Use the Performance view</span><span class="link-card-description">Learn how to use the DevTools performance view.</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-01-favicon.webp" alt=""><span class="link-card-domain">docs.flutter.dev</span></span></span><img class="link-card-image" src="./linkcard-01-image.webp" alt=""></a></div>
        
    
    <figure><img src="./image-001.webp" alt="Dev Tool Performance view" width="1332" height="170"></figure>
    
    -   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](https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html#performance-optimizations)
            
            ```dart
            AnimatedBuilder(
              animation: myAnimation,
              child: MyStaticWidget(), // アニメーションに依存しないサブツリー
              builder: (context, child) {
                return Transform.rotate(
                  angle: myAnimation.value,
                  child: child, // 再ビルドされないサブツリーを再利用
                );
              },
            );
            
            ```
            
-   ツリーの深さや種類を頻繁に変えず、できるだけ同じ構造のままでプロパティだけ変える
    -   ツリー構造が少しでも変化すると、フレームワークが再ビルドや再レイアウト、再ペイントを行う可能性があるため
        -   例）`IgnorePointer` をラップし、プロパティを切り替える
            -   Bad
                
                ```dart
                Widget build(BuildContext context) {
                  if (someCondition) {
                    // 子をそのまま返す
                    return child;
                  } else {
                    // 子を IgnorePointer で包む
                    return IgnorePointer(child: child);
                  }
                }
                ```
                
            -   Good
                
                ```dart
                Widget build(BuildContext context) {
                  return IgnorePointer(
                    ignoring: someCondition, // true or false を切り替える
                    child: child,
                  );
                }
                ```
                
    -   Widget構造を変えなければならないとき は、共通部分を `GlobalKey` 付きのWidgetで包むと、Flutter はその部分を「同一インスタンス」として扱い、不要な再構築・状態リセットを避けられる
        -   `KeyedSubtree` Widgetは、他のWidgetにキーを割り当てることができない場合に便利である
            
-   UI を再利用するときはヘルパーメソッドではなく、新しいWidgetクラスとして切り出す
    -   **Keyやコンストラクタのパラメータ**を使って、Widgetを識別できるようになるため
    -   **変更が一切ないウィジェット**であれば、`const` を使うと**最も高速**に描画・再利用できる
    -   Bad
        
        ```dart
        Widget buildOptionRow(String title) {
          return Row(
            children: [
              Icon(Icons.star),
              Text(title),
            ],
          );
        }
        ```
        
    -   Good
        
        ```dart
        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**](https://youtu.be/IOyq-eTRhvo**)

**参考にした公式ドキュメント**

-   [https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html#performance-considerations](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html#performance-considerations)
    

## `saveLayer()`は慎重に使え

* * *

UIに様々な視覚効果を実装するために、高価な操作である`saveLayer()`を使うものがある

ただし、`saveLayer()` の過剰な呼び出しはJankの原因になる

なぜ、`saveLayer()`は高価なのか？

-   `saveLayer()` を呼び出すと、**オフスクリーンバッファ（裏側の作業用スペース）** が作られる
-   そこに描画した内容を最後に**画面（オンスクリーン）に合成するため、GPU は「メイン画面 → オフスクリーンバッファ → 再びメイン画面」** と描画先を切り替える
-   特にスマホのような**モバイルGPU** は、描画先を切り替えると**パフォーマンスが落ちやすい**
-   できるだけ**画面に直接描画**して**切り替え回数を減らす**と、アプリが軽く動きやすい

**検証方法**

-   DevToolsにおけるPerformanceOverlayLayer.checkerboardOffscreenLayersをチェックする
-   FlutterにおけるMaterialAppのcheckerboardOffscreenLayersをtrueにする
    
    ```dart
    MaterialApp(
        checkerboardOffscreenLayers: true,
        ...
    )
    ```
    

**⚠️要注意**

明示的にsaveLayer()を呼び出していなくても、使っている他のWidgetやパッケージが裏で`saveLayer()`を呼び出している可能性がある

-   内部で`saveLayer()`を使用しているWidgets
    -   [ShaderMask](https://api.flutter.dev/flutter/widgets/ShaderMask-class.html)
    -   [ColorFilter](https://api.flutter.dev/flutter/dart-ui/ColorFilter-class.html)
    -   [Chip](https://api.flutter.dev/flutter/material/Chip-class.html)（`disabledColorAlpha != 0xff` の場合）
    -   [Text](https://api.flutter.dev/flutter/widgets/Text-class.html)（`overflowShader`を使用している場合）
        

## `Opacity`と`Clipping`の使用は最小限にせよ

* * *

`Opacity`と`Clipping`も同様に高価な操作である

**Opacity代用Widget**

-   単純に、0.0 - 1.0の間の不透明度を合成する画像や色が一つだけの場合
    -   `Color.withOpacity`, `Image.withOpacity` を使う
-   画像のフェードイン効果を実装したい場合
    -   `FadeInImage` を使う
-   アニメーションで不透明度を変えたい場合
    -   `AnimatedOpacity` , `FadeTransition` を使う

**Clipping代用Widget**

-   多くのWidgetクラスで提供されている`borderRadius` を使う
    -   例）BoxDecoration

**参考にした公式ドキュメント**

[https://api.flutter.dev/flutter/widgets/Opacity-class.html#performance-considerations-for-opacity-animation](https://api.flutter.dev/flutter/widgets/Opacity-class.html#performance-considerations-for-opacity-animation)

## `Grids`や`Lists`を慎重に実装せよ

* * *

`GridView`や`ListView`は小さなリストではうまく機能するが、多くのアイテムを含むリストを扱うには、`GridView.builder`や`ListView.builder`を使用する

標準の`GridView`や`ListView`では、すべてのアイテムを一度に作成する必要があるのに対し、`GridView.builder`や`ListView.builder`では、画面にスクロールされるたびにアイテムを作成する

## まとめ

* * *

アプリのパフォーマンスを意識して、不要な再ビルドや高価な描画処理を避けることが重要で、フレームワークの仕組みを理解しつつ、Widgetの構成や再利用方法を工夫するだけで大きく変わるのでぜひ実践してみてください！

そしてその結果、パフォーマンスが向上し、ユーザー体験につなげていきましょう！
