Widgetツリー構造とRebuild最適化

2025 年 11 月 4 日 by sumikawah

はじめに

Flutterは「宣言的UI(Declarative UI)」という考え方を採用しています。
これは「状態(State)」が変化したときに、UIをその都度再構築(Rebuild)する仕組みです。
仕組み自体はシンプルで強力ですが、アプリが複雑になると「どこが再描画されるのか」「無駄な再構築が発生していないか」を意識することが重要になります。

本記事では、FlutterのWidgetツリー構造とRebuildの仕組み、そしてパフォーマンスを最適化する考え方について整理します。

Widgetツリーとは?

Flutterの画面は「Widgetツリー」と呼ばれる階層構造で構成されています。
ツリーは大きく以下の3つの層に分かれています。

  • Widget:UIの設計図。軽量な不変オブジェクト(immutable)です。
  • ・Element:Widgetのインスタンスを実際に管理し、ツリー構造を構築する層です。
  • ・RenderObject:実際に描画やレイアウトを担当する層です。

FlutterのWidgetはあくまで「設計図」であり、頻繁に作り直されても大きなコストはかかりません。
重要なのは、Widgetを作り直しても、ElementやRenderObjectがうまく再利用される点です。

Rebuildとは?

Rebuildとは、「状態の変化に応じてWidgetのツリーを再構築すること」です。
たとえば setState() を呼ぶと、FlutterはそのWidget以下のツリーを再構築し、UIを更新します。

// StatefulWidgetの例
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  // カウントの状態
  int count = 0;

  @override
  Widget build(BuildContext context) {
    print('Rebuild: CounterPage'); // Rebuildされるたびにログ出力
    return Scaffold(
      appBar: AppBar(title: const Text('Rebuildの仕組み')),
      body: Center(
        // 現在のカウントを表示
        child: Text('Count: $count', style: const TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        // ボタン押下で状態更新 → build()が再実行される
        onPressed: () {
          setState(() {
            count++;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

上記の例では、ボタンを押すたびに build() が再実行されます。
ただしFlutterは差分をうまく検出しており、再描画の必要がない部分は再利用されます。
この「再構築(Rebuild)」と「再描画(Repaint)」は別物である点も重要です。

Rebuild最適化の考え方

アプリが大きくなると、無駄なRebuildがパフォーマンスに影響を与えることがあります。
ここでは、代表的な最適化の考え方を紹介します。

① const コンストラクタを活用する

不変なWidgetを const 化することで、Flutterは再構築をスキップできます。

② Widgetを小さく分割する

状態を持たないWidgetを分けることで、Rebuildの影響範囲を限定できます。

③ ConsumerWidget / Selectorの利用(RiverpodやProvider)

必要なデータだけを監視し、他のWidgetのRebuildを防ぎます。

④ Hooksを使った局所的な状態管理

HookWidgetで必要な箇所だけ状態を持たせることで、Rebuildの粒度を小さくできます。

実践例:Rebuildの影響を最小限にする

// 表示専用のWidget(StatelessWidget)
// このWidgetはcountの値が変わったときだけ再構築される
class CountDisplay extends StatelessWidget {
  const CountDisplay({super.key, required this.count});
  final int count;

  @override
  Widget build(BuildContext context) {
    print('Rebuild: CountDisplay'); // 再構築の確認用ログ
    return Text(
      'Count: $count',
      style: const TextStyle(fontSize: 24),
    );
  }
}

// 状態を持つ親Widget
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    print('Rebuild: CounterPage'); // 親Widgetが再構築されたか確認
    return Scaffold(
      appBar: AppBar(title: const Text('Rebuild最適化の例')),
      body: Center(
        // CountDisplayを分割することで、他のWidgetが無駄に再構築されない
        child: CountDisplay(count: count),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            count++;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

ポイント:

  • CountDisplay を分割することで、状態を持たない部分が独立し、Rebuildの範囲を限定できます。
  • ・const をつけられる箇所にはできるだけ付けておくと、再構築がさらに減ります。

まとめ

  • ・Flutterは宣言的UIのため、状態が変わるたびにWidgetを再構築する。
  • ・Widgetツリーは「Widget / Element / RenderObject」の3層構造で成り立っている。
  • ・Rebuildは悪いことではなく、仕組みを理解して最小限に抑えることが重要。
  • ・constやWidget分割、ProviderやHooksを活用することで効率的な再構築が可能になる。

タグ: ,

TrackBack