LoginSignup
3
2

More than 1 year has passed since last update.

Flutter Performance best practicesの復習

Last updated at Posted at 2022-12-17

ドキュメント

前提

  • 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 Screenshot_20221213-231430.png
スクリーンショット 2022-12-13 23.26.51.png スクリーンショット 2022-12-13 23.22.58.png

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

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

AnimationBuilder ありコード
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,
        );
      },
    );
  }
}
AnimationBuilder なしコード
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();
  }
}
AnimationBuilder あり AnimationBuilder なし
スクリーンショット 2022-12-14 0.23.37.png スクリーンショット 2022-12-14 0.23.02.png

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

Constを使う

言わずもがなだと思うので省略

ほとんどのプロジェクトでflutter_lints が入っており prefer_const_constructorsが有効化されていると思うので、警告が出たらconstをつける

重たい処理を理解する

saveLayer()

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

s84vd.png

saveLayer()で2つの長方形を合成するコード
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();
}

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

下記のメソッドの内部でsaveLayerが使用されているので、使う時には注意が必要です。

checkerboardOffscreenLayersをtrueにすることで、offscreen layersが使われいてる箇所をアプリで確認することができます。

MaterialApp(
    checkerboardOffscreenLayers: true,
    ...
)

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をアニメーションさせるときは、AnimatedOpacityFadeTransitionを使用することを検討する。

Clip.antiAliasWithSaveLayer

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

ListViewを使うときは画面内の要素のみを描画する

2021年のGoogle I/Oの 「Flutter で実現する遅延読み込みのパフォーマンス」のセッションがわかりやすいので貼っておきます。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2