有状态的小工具のパフォーマンスを考虑した正しい扱い方

FlutterのStateful Widgetは唯一のStateを持つWidgetであり,アプリの状态を保ちながら画面更新する际に必须の存在です。

本记事では具体的な例を用いて,Stateful Widgetをどう扱うのが适切なのかを说明してきます。

以下の记事を书きましたが,これを理解した适合の内容となっています(RenderObject周りはあまり关系しないのでスキップしてもOKです)。

FlutterのWidgetツリーの里侧で起こっていること

小部件,状态,元素,渲染对象の关系とそのライフサイクルを理解する

medium.com


Flutterの公式ドキュメントのStatefulWidgetクラスの后半に,性能考究という项目があって,これをきちんと理解できればOKなのですが,文字だけの说明ですし暗黙的な理解の由此の上で书のいているため,例えばFlutterに触れた直后に読んでも不明点があるのが普通だと思っています。

というわけで,上から訳しながら読み解いていきます。


StatefulWidgets有两个主要类别。

[訳] StatefulWidgetは2种类に大别されます。

1. State.setStateを呼ばないStatefulWidget

第一个是在State.initState中分配资源并将其在State.dispose中处置的资源,但是它不依赖于InheritedWidgets或调用State.setState。 此类小部件通常在应用程序或页面的根部使用,并通过ChangeNotifiers,Streams或其他此类对象与子小部件通信。 遵循这种模式的有状态的小部件相对便宜(就CPU和GPU周期而言),因为它们一旦构建就永远不会更新。 因此,它们可能具有一些复杂且深入的构建方法。

[訳] 1种类目は,以下の特徴を持ちます。

  • State.initStateでリソースの确保をする
  • State.disposeで确保したリソースの解放をする
  • InheritedWidgetsに依存しない
  • State.setStateも呼ばない
  • 变更通知者・流などによって子Widgetとやりとりをする
  • アプリのルートページで使われることが多い

基本的にビルドも1回限りでリビルドが発生ドが発いので,CPU ・ GPU利用率の観点で比较的低コストで済みますが,その代わりに深い子Widgetと络む复雑なビルドの仕组みを持つことがあります。


…と书き下しましたが,いきなり言われてもよく分からないのではと思います。

なぜならFlutterを触り始める际も,公式ドキュメントに従って学习に従って进めていた时に行き着く编写您的第一个Flutter应用程序,第2部分-5。添加交互性においてもStatefulWidgetのsetStateを活用して状态の更新をしていますし,Flutter规ロジェクトを新规作成した际の雏形のCounterアプリも同様です。

一方,この「State.setStateを呼ばないStatefulWidget」は自らサブツリーの更新をsetStateで発火させることはせずに,State(状态)を保持できるという特性を生かして,Stateとライフサイクルを合わせたいオブジェクトの保持や后述のWidgetのキャッシュなどを行います。

もし,ここでまだピンと来なくても,后の例を见てからまた戻って読み直すと理解しやすいと思います。

2. State.setStateを呼ぶStatefulWidget

第二类是使用State.setState或依赖于InheritedWidgets的小部件。 这些通常在应用程序的生命周期内会多次重建, 因此,将重建此类小部件的影响降至最低至关重要。 (它们也可以使用State.initState或State.didChangeDependencies并分配资源,但是重要的是它们可以重建。)

[訳] 1种类目とは逆に,2种类目は以下の特徴を持ちます。

  • State.setStateを呼ぶ
  • InheritedWidgetsに依存することもある
  • アプリのライフタイムの中で何度もリビルドされ得る
  • (1种类目と同様にState.initState ・ State.didChangeDependenciesでのリソース确保をすることもあるがメインの役割はリビルドに頼ったもの)

のリビルドが何度もされ得るという特徴を持つため,リビルドされた时ドされた最小限の必要な处理だけ走るようにするのが大事です。


fulStatefulWidgetの利用例として说明やサンプルコードが多く出回っていますし,イメージしやすいと思います。リビルドのインパクトは,FlutterのWidgetツリーの里侧で起こっていることで说明した通り,以下の处理に相当します。

  1. State.setStateが呼ばれる
  2. がの州以下のWidgetサブツリー全体がリビルド(参照が保持されているWidgetは例外)
  3. 元素サブツリーへの差分适用
  4. RenderObjectサブツリーとの差分チェックをして差分があれば再レンダリング

enderのRenderObjectの差分チェックが理想的に最适化化されていれば,无駄なリビルドが走ドが再レンダリングはされませんが,その直前までの处理(4の「RenderObjectサブツリーとの差分チェック」まで)は毎回确実に走ります。

可以使用几种技术来最大程度地减少重建有状态的小部件的影响:

[訳]有状态的小工具のリビルドの影响を最低限にするための様々なテクニックがあります。

というわけで,このあと箇条书きでそれらのテクニックが罗列されていきますが,それぞれ丁宁に见ていきます。


把状态推到叶子上。 例如,如果您的页面上有一个滴答时钟,而不是每次在时钟滴答时都将状态置于页面顶部并重新构建整个页面,则创建一个专用的时钟小部件,该小部件只会自我更新。

[訳]状态を末端に追いやるとサブツリーがその末端以下となって小さくなるのでその分リビルドが軽くなります。
例えば,Widgetが时间とと更新もに时计を持っていたとして,その时计をルートで保持すると时计の更新のたびにページ成员が无駄にリビルドされることドされるこ。その代わりに时计のWidgetを别途作って末端に置いて自律的に更新させるようにすると良いです。

めに,初めに书いてあるだけあって理解しやすいですし,确実に效果がありそうです。

まずは,ページ全体が无駄にリビルドされるというダメなパターンです。

そして次のコードがリビルドを末端ドを追いやった改善例です。

パフォーマンスが良くなるだけでなく,时计表示に关する部分がClocPageからClocに切り出され(実clock.dartにはさらにclock.dartなどの别ファイルにすると思います),それぞれのWidgetの役割分担が明确になって可読性・メンテナンス性も上がっています。


最小化由build方法及其创建的任何小部件短暂地创建的节点数。 理想情况下,有状态小部件将仅创建一个小部件,而该小部件将是RenderObjectWidget。 (显然,这并不总是可行的,但是小部件越接近于这种理想状态,它将越有效。)

[訳] buildメソッドで返ドでWidgetすリーから生成されるノードの総数は极力减らした方が良いです。理想的にはbuildメソッドではたった1つのRenderObjectWidgetを返すようにするとすようにするフォーマンス的に最も有利です(実际のアプリではそこまで追い込めるわけではなく,あくまで理想)。

まず,ここで言う理想的なStateful Widgetとは例えば次のようなものです。

本来statefulであるべきなのは不透明度小工具のopacityのみです。そこで上述のテクニックのようにそこだけをStateful Widgetに切り出してみましょう。
(后述の通り,フェードインアニアニメーションはもっと简単に书ける便利なWidgetがあります。)

ちなみに,上では说明のために愚直にアニメーションを组みましたが,以下を使うとスマートに书けますし,

  • AnimatedWidget
  • 动画制作器

不透明度のアニメーションショ简を书けるWidgetもあるので,実际にはこれら活用した方が良いです。

  • 动画不透明度
  • 渐隐过渡

これらをお行仪よく使えば简単に无駄の无いアニメーション実装ができます。

また,关连として,PageRouteの具象クラスを使ってアニメーション付きの画面迁移がなされますが,それらも同様のケアがされているので迁移対象の画面のリビルドが大量に走ることは无いです。

キャッシュすると思わぬバグの要因になったりしない?

Widgetはビルドの度に毎回生成するのが当たり前の感覚を持っていると,キャッシュするのが微妙な気がするかもしれません。

例えば,状态のフィールドにキャッシュしているWidgetの中身を途中で书き换えてしまうと,「更新したつもりなのに反映されない」という事态になりそうです🤔

そ,まずWidgetは@immutableで,お行仪よく书かれていればフィールドはすべてfinalになっています。そのため,「Widgetの中身を途中で书き换えてしまう」操作自体ができません。

一方,次のようなWidgetの列表のキャッシュの场合はどうでしょうか?

ListViewの场合はListの参照の同が场合は更新ListListに変えるしないという最适化が入っていてそれに阻まれているためです。

というわけで,キャッシュに由来するトラブルは基本的にはあまり起こらなさそうではあるものの,场合によっては少し注意かなと思います。

  • 混乱一のWidgetの场合はキャッシュしたことによって混乱することは无さそう(混乱招くようなコードを书けない)
  • 列表などWidgetを间接的に持っている场合は,それを受け取るWidget次第で挙动が変わったりするので注意(単一のWidgetと同様基本的には途中で弄らない方が良いと思う)

尽可能使用const小部件。 (这等效于缓存小部件并重新使用它。)

[訳] constキーワードをなるべく使う(Widgetをキャッシュして再利用するのと同等)。

括弧付けで书いてある通り,上に书いたキャッシュと同等の效果があります。

例えば,まず次のように毎秒setStateが呼ばれるPageがあったとします。NonenseWidgetという无駄に色々入れ子にしたWidgetをbuildで返しています。

上记コードに対して,次のようにするだけでリビルドされるのがPageだけになって状况が剧的に改善します。

  • NonsenseWidgetにconstコンストラクターを定义
  • _PageStateのbuildメソッドでconstを指定

constで生成できるWidgetは,コンパイル时に确定できるものに限られます。

つまり, constを付けるだけでリビルドを防げるため一见使い胜手が良く见えますが, constコンストラクターにしてかつ利用侧もことになると思います。

ちなみに,以下の记事ではネストの深くなってしまったbuildメソッドをプライベートメソッドに分割しても単に见た目がすっきりするだけなので,パフォーマンス向上のためにはWidgetとして切り出してconstを指定する必要があるということが书かれていて,同様のことを言っています。

将小部件拆分为方法是一种性能反模式

最初于2018年12月11日发布在iirokrankka.com。

medium.com

ネストが深くなったbuildメソッドを持つWidgetはリビルドのインパクトが大きくなりがちで,それを軽减それを軽とは良いプラクティスなのでその通りなのですが,继承の状态のフィールドでキャッシュさせる方法に触れられていないの実惜しいです。実际には,以下の3つから适したものを选ぶのが良いと思っています。

  • そもそもライベートメソッドに切り出す(そもそもStateに依存していたら设计を変えない限りこうするしかない)
  • 州立フィールドでキャッシュ(メソッド分割と同程度に简単に书ける)
  • Widgetとして切り出してconstを指定する(他でも利用するようなWidgetならこれが良さそう)

constの指定し忘れを防ぐには?

実は,これまで书いてきたところの様々なところでconstを指定する余地があって,基本的にはそれら指定できるところには指定しておいた方がベターだと思っています(例では他のテクニックの说明と混ざるのを避けるためにあえて无指定にしていました)。

とはいえ,Dart 2でnewを省略できるようになったこともあり,つい无指定で书いてしまうことがありがちだと思いますが,それは静的解析で解决できます。

Dartプロプクトのanalysis_options.yamlをトップレベルに置くとその内容に応じて静的解析がかかりますが,以下のルールを指定すると, const付け忘れを指摘してくれるようになってオススメです。

  • preferred_const_constructors
  • preferred_const_literals_to_create_immutables

自定义静态分析

使用分析选项文件定制静态分析。

www.dartlang.org

preferred_const_literals_to_create_immutablesのルールが效くのは,Widget抽象クラスには@immutableが付いているからです。

constはトップレベルに1つ付ければOK

上の例では次のようにNenseenseWidgetには付けずに一番上のScaffoldにconstを付けています。

 返回const脚手架( 
身体:中心(
子级:NonsenseWidget(),
),
);

artDart 2で以下の改善がなされたからです。

  • newは常にオプション
  • constは定数コンテキスト上ではオプション

飞镖语言之旅

游览Dart的所有主要语言功能。

www.dartlang.org

详しくは,公式ドキュメントのコンストラクターの项目を见てください。

ちなみに,「Dart2でnewやconstが省略できる」という记事でconstも完全に省略してOKのような记述があったり同様な误解をしている人が结构いそうな気がしていますが,それは误りなはずで, constは必要に応じて最低限の明示が必要です。

上の记事の以下の例ではEdgeInsetsconstを省略してしまっていますが,そこはnewの容器コンテキスト内なので同じくnew扱いになって, const明示した场合と比べてパフォーマンスが落ちてしまいます。
(この记事というより元のセッション动画でのスライド时点で间违っているようですが。)

 返回容器( 
高度:56.0,
填充:EdgeInsets.symmetric(水平:8.0),
...
);

Containerなどのconstが使えないWidgetの代わりにconstを使えるWidgetが使えないか検讨する

Flutterにおいて色々できて便利なContainerですが,次の理由で乱用はするべきではないです。

  • 容器のコンストラクターはconstではない
  • 用途により适したWidgetがある

容器は色々な使い方に対応できるようになっているためかコンパイル时に决定することができず,constコンストラクターが提供されておらず,利用时にconst指定ができません。

など,以下など目的に沿ったWidgetがあって,これらにはconstコンストラクターが提供されています。

  • SizedBox:要素间にスペースを空けたい时など
  • 填充:文字通りパディングを设けたい时
  • 对齐:要素を寄せたい时

で使うべきシーンで容器を使っていて,パフォーマンスも可読性も损なってしまっている例をよく见ますが,より目的に适したWidgetを使い分けるべきです。constの有无を除いても単一の责务に持つこれらのWidgetに比べて容器は重いです。

一方,容器が适した场面ももちろんあり,そこで使うのは当然全く否定していません。

constのWidgetも変更に追従できる

以上のように, constを付けると设计図にあたるWidgetはコンパイル时に确定して1回ビルドしたインスタンスがそれ以降使い回されるようになります。

こう书くと,表示が全く変わらない部分にしか使えないように感じますが,そうではなく动的な画面にも活用できます。

色々やり方はありますが,InheritedWidgetを使ったやり方が一番の基本です。次のようにconstのNonsenseWidgetにInheritedWidgetを継承したTimeInheritedを挟みます。

可能かというと,InheritedWidgetは以下の特性を持つからです。

  • InheritFromWidgetOfExactTypeによってO(1)のアクセスができるとともにその上下文もにそ自身の変更を伝播してリビルドを発火できる
  • updateShouldNotifyにより,自身の保持しているデータに応じて実际に変更を伝播するかの制御も可能

NonsenseWidgetはbuildメソッドでinheritFromWidgetOfExactTypeを使っているので,次回TimeInheritedの保持しているtimeが更新された时にまたリビルドが要求されます。

つまり,InheritedWidget依存を静的に宣言しているconst Widgetなのでそれ自体は不変だが,自身のサブツリーはInheritedWidgetからの変更通知によってリビルドできる,ということになります。

整理すると,constとInheritedWidgetの组み合わせで次のようなことが実现できるということです。

  1. constで亲ツリーのリビルド伝播をせき止める
  2. InheritedWidgetでそのconstでせきとめられているツリー配下のサブツリーのリビルドができる

ちなみに,この组み合わせは,ReactのshouldComponentUpdateと同等の机能を担っているという认识です。

(const)WidgetにInheritedWidgetを使って変更伝播する方式はFlutterフレームワークでもよく用いられている

例えば,次のように,主题小工具の中にあるconstのTextが,テーマの変更に応じて切り替わるのも,主题周りの内部実装としてInheritedWidgetが使われているからです。

実际のアプリ开発でInheritedWidgetをそのまま使うことは少ない気がする

直接继承的Widgetを使うとコードが复雑になってくるので,実际のアプリ开発ではscoped_modelなど便利にラップしたものを使うのがおすすめです(とはいえInheritedWidgetの生の动きを把握していることも大事です)。

scoped_model | 颤振包

scoped_model Flutter和Dart程序包–一个将Reactive Model传递给所有子级的小部件

pub.dartlang.org

完全に余谈ですが,scoped_modelは0.3と1.0で内部実装がガラリと変わって利用しているAnimatedBuilder内で暗黙的にsetStateする実装に変わっていて面白かったです。
(0.3以前の方がソースとしては読みやすく,1.0でテクニカルになった感じです。)

以下のように検证したところ,

  • Elementツリー再作成されているか検证: MyContainerのcreateElementがいつ呼ばれるかを探る
  • 再ペイント处理が走るか検证: RenderObjectのmarkNeedsPaintメソッドが呼ばれるかどうかブレークポイントをはる

结果は,次のようになり,予想通りでした。

  • いかなる操作をしてもcreateElementは起动直后の一回しか呼ばれない
  • 右下のFABタップでIgnorePointerの忽略プロパティを切り替えてリビルドしても再ペイント处理は一切走らない

一方,非推奨な,サブツリーの构造を変えるやり方にするためにbuildメソッドを次のように书き换えて试すと,右下のFABBOタンを押すたびにMyContainerのcreateElementが呼ばれて,もちろん再ペイント处理もこちらも。こちらも同じく予想通りの结果でした。


如果由于某种原因必须更改深度,请考虑将子树的公共部分包装在具有GlobalKey且在有状态小部件的生命周期中保持一致的小部件中。 (如果无法方便地为其他小部件分配键,则TheKeyedSubtree小部件可能对此有用。)

[訳]かしら何かしらの理由でサブツリーの深さを変えたい场合,サブツリーの主要パーツを,GlobalKeyをもつStateful Widgetでラップすると良いです。 Widgetがこの用途に适しているでしょう。)

先での非推奨の例について,MyContainerにGlobalKeyを渡しているのが以下です。KeyでBuildContextを通してElementおよびRenderObjectの参照とそのサブツリーを保持するのがポイントです。

,行结果を见ると,こうやってKeyを指定するだけで,MyContainerのcreateElementは1度しか呼ばれないようになったので,ElementツリーおよびRenderObjectツリーが维持されていることが分かりました。一方, _ignoringを切り替えてsetStateするとさすがに再ペイント处理は走ってしまったので,ツリー构造を保つパターンと比べると少し劣るようです。とはいえ,ドキュメントの言う通り,构造を変えつつもパフォーマンスを维持するパターンとして有用そうです。

括弧で书かれているKeyedSubtreeは,Keyを受け付けられないWidget(お行仪悪い気がしますが)に対してKeyを差し込みたい场合に以下のように使えます。

 子级:KeyedSubtree( 
键:_key,
子代:MyContainer(颜色:_colors [_current%3]),
),

GlobalKey与えるだけでパフォーマンス上がるなら,各所でもっと积极的に使っていくと良いのか?というとそんなことはなく,GlobalKeyの管理は高コストでありとであり影响する及ぼしかねないので,基本的にはビ无しで済ませるべきです。リビルドインパクトの大きなツリーの保持をしたいがGlobalKeyを指定する以外の方法が无い时など,限定的に使いましょう。
(GlobalKeyはパフォーマンス観点其他に下位ツリーの状态参照したい时などにも必要で,もちろんそういう场合もOKです。)

全局密钥相对昂贵。 如果不需要上面列出的任何功能,请考虑改用Key,ValueKey,ObjectKey或UniqueKey。

https://docs.flutter.io/flutter/widgets/GlobalKey-class.html


これまでも,Fl长でようやくなぞり,以上をするにあたり终ance。これまでも,Flutterの勉强をするにあたり性能考量を何回か読んでいか読んで,今回この记事を书くにあたって隅々まで正确に理解しようとしながら読み直してとても勉强になりました。

まず,优先度観点としては,个人的には以下程度で良いと思います。
(项目や顺番は厳密なものではなく,パフォーマンスが一番下ということが主旨です。)

  1. 可読性
  2. メンテナンス性
  3. 実装の手间
  4. パフォーマンス

ではなく何かを犠牲にしてまで优先させるべきものではなく,それでもどうしてもパフォーマンスを优先したい时に渋々パフォーマンスチューニングするものだと思っています。

また闻きですが,こちらの@k_katsumiさんのコメントに同意です。

…という加压で,何かを犠牲畜にすることなく,可読性・メンテナンス性などとパフォーマンを両スマ立できるものも多いと思っていて,本记事の项目の大半もそれにあたると思っています。

つまり,パフォーマンスを过度に证明に必要はない(むしろ良くない)ですが,実际にどういう处理が走るのかを意识しながら,そのケースにトにルパ。パフォーマンス的に优れた书き方を知っている上であえて崩すとと,よく分からないままに适当に书くのとでは,天と地の差があります。

になってしまったりする,雑に书いていたらスクロールやアやンショカカカカクになってしまったりする原因の大半は,パフォーマンスの优先度が低いというより技术的に至ってなかっただけなことが原因だと思っていて,それをそれを懒って未熟な状态で実装を进めても,パフォーマンス周りの后追い対応に追われて结局无駄に时间食うということになりがちだと思っています。

,ではよく分からない疑问点のほとんどが解消できてとても良かったです。


本记事でなぞったのは次のうち前者のState fulの方でした。

  • 有状态的Widgetに记にいる性能注意事项
  • 少说 Widgetに记されている性能注意事项

状态の方が高机能なので,それをなぞれば状态の方もついでに大体カバーできてしまった感がありますが,微妙に抜けている点もあるので,兴味があれば状态の方も読んでみてください。


は贴ったGistはmain.dartファイルとして完全ではないものがありますが,以下のリポジトリー内に动作する完全なサンプル群が入っています。

mono0926 / dive_into_flutter

通过在GitHub上创建一个帐户,为mono0926 / dive_into_flutter开发做出贡献。

github.com