Kodokon kodokon.com

性能:const、重建、key 与 DevTools

借助 const、key 和协调机制消除无用的重建,然后用 DevTools 进行测量。

10 分钟 · 3 题

在 Kodokon 中打开本课

Flutter 操纵着三棵树:widget(不可变的、用完即弃的配置)、Element(承载状态的活实例)以及渲染对象(布局、绘制)。重建一个 widget 是廉价的;真正昂贵的是布局和层层传导的绘制。在一次重建过程中,Element.updateChild 会按顺序应用三条规则:如果新的 widget 与旧的完全相同(同一个实例),整棵子树就被短路;如果 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 跳过了子树。

对于子节点列表,协调机制首先按位置来配对。在没有 key 的情况下对有状态的条目重新排序,每个 Element 都会保留它这个位置上前一位占据者的状态:勾选框勾错了地方,文本框被张冠李戴。一个 Key 会改变配对规则:即便条目在列表中移动了,算法也能找到那个携带相同 key 的 Element。请在一个稳定的业务标识符上使用 ValueKeyGlobalKey 走得更远,它让你能够在不丢失状态的情况下把整棵子树移到新的父节点下,但它有实实在在的代价(全局注册表、贯穿整棵树的唯一性约束):把它留给像 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)),
          ),
      ],
    );
  }
}
没有稳定的 key,Dismissible 一删除就会抛出错误。

先测量,再优化。在 DevTools 中,Performance 视图会追踪两个关键线程每一帧的耗时:UI(你的 Dart 代码:build、layout)和 raster(把图层栅格化)。在 60 Hz 下,预算大约是 16 毫秒。raster 一侧的卡顿靠优化你的 build 是修不好的:它来自绘制的开销(阴影、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 widget 会被忽略?
    • 这个实例是规范化的:identical 为真,Element.updateChild 会短路整棵子树
    • 编译器在 release 中把这个 widget 从树里移除了
    • const widget 没有 build 方法
  2. 你在没有 key 的情况下对一列有状态的 widget 重新排序。会发生什么?
    • Element 是按位置配对的:每个条目都继承了它这个位置上前一位占据者的状态
    • Flutter 在 debug 模式下抛出一个异常
    • 状态会自动跟着每个被移动的 widget
    • 这个列表拒绝重建
  3. 性能浮层的那两条图表测量的是什么?
    • UI 线程(Dart 代码)和 raster 线程(图层渲染)每一帧的耗时
    • 已分配的内存和 widget 的数量
    • 构建耗时和垃圾回收耗时