Kodokon kodokon.com

パフォーマンス:const、再構築、キー、DevTools

const、キー、そして照合のおかげで無駄な再構築をなくし、それからDevToolsで計測しましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

Flutterは三つのツリーを扱います。ウィジェット(不変で使い捨ての構成)、Element(状態を運ぶ生きたインスタンス)、そしてレンダーオブジェクト(レイアウト、描画)です。ウィジェットの再構築は安上がりです。コストがかかるのはレイアウトと連鎖する描画です。再構築のあいだ、Element.updateChildは三つのルールを順番に適用します。新しいウィジェットが古いものと同一(同じインスタンス)なら、サブツリー全体がショートカットされます。Widget.canUpdateが真なら(同じruntimeType、同じkey)、Elementは保持され、単に再構成されます。そうでなければ、Elementは破棄され、それから状態も含めて再展開されます。constキーワードは最初のルールを活用します。定数式はコンパイラによって正規化され、ビルドごとに同じインスタンスが返り、identicalが真になって、枝全体がスキップされます。

DART
import 'package:flutter/material.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() =>
      _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _Header(),
        Text('Total: $_count'),
        FilledButton(
          onPressed: () => setState(() => _count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    debugPrint('build _Header');
    return const Text('Counter');
  }
}
ログは一度だけ現れる。constがサブツリーをスキップする。

子のリストでは、照合はまず位置によってペアを組みます。状態を持つ要素をキーなしで並べ替えると、各Elementはその位置の前の住人の状態を保ってしまいます。チェックボックスが間違った場所でチェックされ、テキストフィールドが入れ替わります。Keyはペアの組み方のルールを変えます。アルゴリズムはリストの中で移動しても、同じキーを持つElementを見つけます。安定した業務上の識別子にValueKeyを使いましょう。GlobalKeyはさらに進んで、状態を失うことなくサブツリー全体を親付け替えできるようにします。ただし本物のコスト(グローバルなレジストリ、ツリー全体にわたる一意性の制約)を伴うので、Formや親付け替えのような本当に必要な場面のためにとっておきましょう。

DART
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
  const TodoList({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (final item in items)
          Dismissible(
            key: ValueKey(item),
            onDismissed: (direction) {},
            child: ListTile(title: Text(item)),
          ),
      ],
    );
  }
}
安定したキーがなければ、Dismissibleは削除するとすぐにエラーを投げる。

最適化する前に計測しましょう。DevToolsでは、Performanceビューが二つの重要なスレッドのフレームごとの時間を追跡します。UI(あなたのDartコード。ビルド、レイアウト)とラスター(レイヤーのラスタライズ)です。予算は60 Hzで約16 msです。ラスター側のジャンクは、ビルドを最適化しても直りません。描画のコスト(影、saveLayer、大きすぎる画像)から来るものだからです。誰が再構築しているかを見るにはTrack widget buildsを有効にし、再描画された領域を可視化するにはdebugRepaintRainbowEnabledを有効にしましょう。再描画のたびに枠の色が変わります。RepaintBoundaryはサブツリーを独自のレイヤーへと隔離します。絶え間なく再描画するスピナーが、もう隣人を汚さなくなります。

DART
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  debugRepaintRainbowEnabled = true;
  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      showPerformanceOverlay: true,
      home: Scaffold(
        body: Center(
          child: RepaintBoundary(
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}
フレームのオーバーレイと再描画のレインボー。診断の二人組。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. constウィジェットは、親が再構築されるときになぜ無視されるのですか?
    • インスタンスが正規のものです。identicalが真になり、Element.updateChildがサブツリー全体をショートカットする
    • コンパイラがリリースでウィジェットをツリーから取り除く
    • constウィジェットにはbuildメソッドがない
  2. 状態を持つウィジェットのリストをキーなしで並べ替えます。どうなりますか?
    • Elementは位置によってペアを組みます。各要素はその位置の前の住人の状態を受け継ぐ
    • Flutterがデバッグモードで例外を投げる
    • 状態が移動した各ウィジェットに自動的についていく
    • リストが再構築を拒む
  3. パフォーマンスのオーバーレイの二つのグラフは何を計測しますか?
    • UIスレッド(Dartコード)とラスタースレッド(レイヤーのレンダリング)のフレームごとの時間
    • 割り当てられたメモリとウィジェットの数
    • ビルド時間とガベージコレクションの時間