Flutter Performance best practicesの復習

## ドキュメント
https://docs.flutter.dev/perf/best-practices

## 前提
- Flutterの公式のドキュメントのパフォーマンスの章の内容を復習する
- よく遭遇するパターンを掻い摘んでまとめます
- 説明をスムーズに行うために別のドキュメントの内容も一部記載する

## 環境
```
[✓] Flutter (Channel stable, 3.3.9, on macOS 12.6 21G115 darwin-arm
    (Rosetta), locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK
    version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.0.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.3)
[✓] Connected device (3 available)
[✓] HTTP Host Availabilit
```


## setStateが呼ばれると小孫のWidgetがrebuildされる

Flutterのサンプルプロジェクトでボタンとテキストを別のWidgetに切り出した場合、rebuildの範囲は切り出した子のWidget内に止まります。
ボタンを押した後の `Widget rebuild stats` を見るとAppBarなどはrebuildされていないことがわかります。

| Sample | 切り出した場合 |
|:--: | :--: | 
| ![Screenshot_20221213-231245.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/b70b4fdd-fc02-7321-59e4-f5a4dcf86a00.png)  |  ![Screenshot_20221213-231430.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/5bd46082-3274-721a-f11d-2af74bf15af6.png) |
| ![スクリーンショット 2022-12-13 23.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/50f193df-8ddc-877c-0fe4-de2e55514a12.png)| ![スクリーンショット 2022-12-13 23.22.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/61ffb16e-2de7-3e98-d120-0ea70f9c6e67.png)|

## 同じインスタンスを使うことで無駄なrebuildを抑える

AnimationBuilderの実装パターンのように、小孫のWidgetで同じのインスタンスを使うことで子のWidgetのrebuildが発生しない。FlutterのアニメーションWidgetでは、アニメーションの影響を子のWidgetに影響を与えないようによく使われる実装パターンです。


<details> <summary>AnimationBuilder ありコード</summary>

```dart
class _MyStatefulWidgetState extends State<MyStatefulWidget>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 10),
    vsync: this,
  )..repeat();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: Container(
        width: 200.0,
        height: 200.0,
        color: Colors.green,
        child: const Center(
          child: Text('Whee!'),
        ),
      ),
      builder: (BuildContext context, Widget? child) {
        return Transform.rotate(
          angle: _controller.value * 2.0 * math.pi,
          child: child,
        );
      },
    );
  }
}
```

</details>

<details> <summary>AnimationBuilder なしコード</summary>

```dart
class _MyStatefulWidgetState extends State<MyStatefulWidget> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
      duration: const Duration(seconds: 10),
      vsync: this,
    );
    animation = controller
      ..addListener(() {
        setState(() {});
        // #docregion addListener
      });
    // #enddocregion addListener
    controller.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: animation.value * 2.0 * math.pi,
      child: Container(
        width: 200.0,
        height: 200.0,
        color: Colors.green,
        child: const Center(
          child: Text('Whee!'),
        ),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}
```

</details>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/b2f2e6bb-3935-8465-70c2-8009ead79724.gif" width="300px">

| AnimationBuilder あり | AnimationBuilder　なし |
|:--: | :--: | 
| ![スクリーンショット 2022-12-14 0.23.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/f2f0df1b-2a4a-7f2d-39ac-7a47d850ddd4.png) | ![スクリーンショット 2022-12-14 0.23.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/dbf312b6-413e-2b69-b539-1183034a66e0.png) |

AnimationtBuilderを使ったコードでは、_MyStatefulWidgetStateがrebuildされていないことがわかります。


## Constを使う
言わずもがなだと思うので省略

ほとんどのプロジェクトで[flutter_lints](https://pub.dev/packages/flutter_lints) が入っており [prefer_const_constructors](https://dart-lang.github.io/linter/lints/prefer_const_constructors.html)が有効化されていると思うので、警告が出たらconstをつける

## 重たい処理を理解する

### saveLayer() 

panit処理で複数のレイヤーを合成し複合効果を持たせるときに使う[saveLayer()](https://api.flutter.dev/flutter/dart-ui/Canvas/saveLayer.html) を過度に呼び出すと、ジャンクが発生する原因となります。saveLayer()はFlutterフレームワークで最も高価なメソッドの1つです。

![s84vd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/7cd353a3-9b86-7bc9-afe8-93e9a171571e.png)


<details> <summary>saveLayer()で2つの長方形を合成するコード</summary>

```dart
void paint(Canvas canvas, Size size) {
  Rect rect = Offset.zero & const Size(100, 100);
  canvas.drawRect(
    rect,
    Paint()..color = Colors.red,
  );
  canvas.saveLayer(
    null,
    Paint()..blendMode = BlendMode.multiply,
  );
  canvas.drawRect(
    rect.shift(const Offset(20, 20)),
    Paint()..color = Colors.blue,
  );

  canvas.restore();
}
```
</details>


saveLayer()を使用するとコンテンツをオフスクリーンバッファーに描画する必要があり、レンダーターゲットの切り替えが発生する場合があります。レンダーターゲットの切り替えにより、大量のメモリチャーンが発生し、アプリが遅くなる可能性があります。


下記のメソッドの内部でsaveLayerが使用されているので、使う時には注意が必要です。
- [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) (TextOverflow.fadeが使われているとき)

[checkerboardOffscreenLayers](https://api.flutter.dev/flutter/rendering/PerformanceOverlayLayer/checkerboardOffscreenLayers.html)をtrueにすることで、offscreen layersが使われいてる箇所をアプリで確認することができます。

```dart
MaterialApp(
    checkerboardOffscreenLayers: true,
    ...
)
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209811/23dceba9-023a-516a-31d7-cd94b0b06289.png" width="200px">

https://docs.flutter.dev/perf/ui-performance#checking-for-offscreen-layers


### Opacity
Opacityはコストのかかる操作になります。
Opacity Widgetを使用せずに直接Opacityをかける方が高速です。

- Colorの例
    - GOOD: `Container(color: Color.fromRGBO(255, 0, 0, 0.5))`
    - BAD: `Opacity(opacity: 0.5, child: Container(color: Colors.red))`
- Imageの例
    - GOOD: `Image.network('https://hoge.jp/icon.jpeg', color: const Color.fromRGBO(255, 0, 0, 0.5))`
    - BAD: `Opacity(opacity: 0.5m child: Image.network('https://hoge.jp/icon.jpeg', color: const Color.fromRGBO(255, 255, 255, 0.5)))`

Opacityをアニメーションさせるときは、[AnimatedOpacity](https://api.flutter.dev/flutter/widgets/AnimatedOpacity-class.html)や[FadeTransition](https://api.flutter.dev/flutter/widgets/FadeTransition-class.html)を使用することを検討する。

### Clip.antiAliasWithSaveLayer
Opacityほどコストのかかるではありませんが、コストのかかる操作になります。
角丸の実装などではClipを使わずに、borderRadiusを使うようにする


## ListViewを使うときは画面内の要素のみを描画する
2021年のGoogle I/Oの 「Flutter で実現する遅延読み込みのパフォーマンス」のセッションがわかりやすいので貼っておきます。

https://io.google/2021/session/db72613f-8e07-4be8-80c1-174dd95730fc?lng=ja