【Flutter】Flutterアプリの動作を高速化!すぐに実践できるパフォーマンス改善術
はじめに
こんにちは。株式会社メンバーズCross Applicationカンパニーの浅野です。
Flutter開発をしていて、画面のカクつきや動作が重く感じたことはありませんか?
今回は難しい設定不要で、すぐに導入できるパフォーマンス改善テクニックを紹介します。
できる限りconstコンストラクタを使う
ウィジェットを使用する際にconstコンストラクタを使うことで、ウィジェットが再構築されるたびに毎回インスタンスを作らなくて済むため、リビルド時のコストを軽減することができます。
// Bad
Widget build(BuildContext context) {
return Text('Hello');
}
// Good
Widget build(BuildContext context) {
return const Text('Hello'); <-- constを使う
}
詳細は割愛しますが、Flutterにはフレームワーク内で更新すべきウィジェットを効率的に決定し、必要な部分だけを再構築する仕組みがあります。その中で使われているElementAPIのupdateChildメソッドでは以下のような処理が行われており、新旧ウィジェットが同一インスタンスと判断された場合は、ポインターを再割り当てするだけの簡単な処理となっています。
constを使用して新旧ウィジェットを同一と判断させることでフレームワークの処理を軽減させ、パフォーマンスを向上させることが期待できます。
// updateChildメソッドの一部抜粋
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
// 省略
if (hasSameSuperclass && child.widget == newWidget) { <-- 新旧ウィジェットのインスタンスを比較
...
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
...
child.update(newWidget); <-- 同一でない場合はupdateの処理が発生してしまう
...
newChild = child;
}
...
return newChild;
}
ウィジェットをキャッシュする
場合によってはconstコンストラクタを使用できない場合もあると思います。そのような場合はStateクラス内でウィジェットをキャッシュすることで、constと同様の効果を得ることができます。
class _MyWidgetState extends State<MyWidget> {
// constが使えないウィジェットをキャッシュする
final _blueContainer = Container(color: Colors.blue, height: 100, width:100);
@override
Widget build(BuildContext context) {
return Column(
children: [
_blueContainer, <-- キャッシュしたウィジェットを使用
Container(
color: Colors.red,
height: 100,
width: 100,
),
],
);
}
}
上記のようなコードの場合、buildメソッドが実行されると赤のコンテナはbuildのたびに再生成されますが、State内でキャッシュしている青のコンテナは再生成されません。
これは1で紹介したupdateChildメソッドで同一のウィジェットと判断されるため、constコンストラクタを使った場合と同様に、不要なリビルドを防ぐことができるためです。
どのような場合でも使えるものではありませんが、上記のようにbuildごとに内容は変わらないが、constコンストラクタが使用できないような場合に使えると思います。
ウィジェットの分割でリビルド範囲を最小化する
StatefulWIdget内でsetStateを呼び出して状態を更新することはよくあると思います。その際にウィジェットツリーが大きくなっていると、少しの変更でも全体がリビルドされてしまいパフォーマンスの低下を招きます。
更新されるウィジェットを分割することでリビルドされる範囲を最小化し、パフォーマンスを向上させることができます。
class _MyWidgetState extends State<MyWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'カウント:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
TextButton(
onPressed: _incrementCounter,
child: const Text('増やす!'),
)
],
),
),
);
}
}
上記の場合、ボタンをタップしてカウントを増やすとsetStateで再ビルドが実行され、カウントの数が増加します。しかし、更新したいのはカウントの部分だけでも、buildメソッド内の全体がリビルドされてしまい不要なリビルドが発生しています。
そこで、以下のようにウィジェットを分割することでリビルド範囲を最小化します。
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: const Center(child: CounterText()),
);
}
}
// リビルドが発生するウィジェットを分割する
class CounterText extends StatefulWidget {
const CounterText({super.key});
@override
State<CounterText> createState() => _CounterTextState();
}
class _CounterTextState extends State<CounterText> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(...省略);
}
}
上記のようにウィジェットを分割することで、リビルド範囲がCounterTextウィジェット内に限定されるため、不要なリビルドを削減することができ;ます。
まとめ
今回紹介したテクニックは、どれもすぐに導入できる簡単なものです。
何気ないウィジェットの書き方でも、ちょっとした意識や工夫でアプリのパフォーマンスを向上させることができます。
もっと詳しい情報が欲しい方は、以下の公式ドキュメントも参考にしてみてください。
参考
https://qiita.com/chooyan_eng/items/ec11f6dcf714f7a2fa3d
miola , alberto. (2023). Flutter Complete Reference 2.0: The Ultimate Reference for Dart and Flutter.
Orlova , D., Kadah , E., & Blasco, J. (2024). Flutter Design Patterns and Best Practices.
この記事を書いた人

Advent Calendar!
Advent Calendar 2024