Kodokon kodokon.com

ประสิทธิภาพ: const, การสร้างใหม่, key และ DevTools

กำจัดการสร้างใหม่ที่ไร้ประโยชน์ด้วย const, key และการปรับให้ตรงกัน (reconciliation) แล้ววัดผลด้วย DevTools

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

Flutter จัดการกับ tree สามต้น: วิดเจ็ต (การตั้งค่าที่เปลี่ยนแปลงไม่ได้และทิ้งได้), Element (อินสแตนซ์ที่มีชีวิตซึ่งพกพาสเตตไว้) และ render object (เลย์เอาต์, การวาด) การสร้างวิดเจ็ตใหม่นั้นราคาถูก สิ่งที่แพงคือเลย์เอาต์และการวาดที่ลามต่อกันไป ระหว่างการสร้างใหม่ Element.updateChild จะใช้กฎสามข้อตามลำดับ: หากวิดเจ็ตใหม่ เหมือนกันเป๊ะ (อินสแตนซ์เดียวกัน) กับตัวเก่า ทั้ง subtree จะถูกลัดวงจรข้ามไป หาก Widget.canUpdate เป็นจริง (runtimeType เดียวกัน, key เดียวกัน) Element จะถูกเก็บไว้และเพียงแค่ตั้งค่าใหม่ ไม่เช่นนั้น Element จะถูกทำลายแล้วสร้างขึ้นมาใหม่ (re-inflate) รวมถึงสเตตด้วย คำสงวน const ใช้ประโยชน์จากกฎข้อแรก: นิพจน์คงที่จะถูก ทำให้เป็นรูปแบบมาตรฐาน (canonicalize) โดยคอมไพเลอร์ อินสแตนซ์เดิมจะกลับมาในทุกการ build ทำให้ 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');
  }
}
log ปรากฏเพียงครั้งเดียว: const ข้าม subtree ไป

สำหรับรายการของ children การปรับให้ตรงกันจะจับคู่ตาม ตำแหน่ง ก่อน หากคุณจัดเรียงสมาชิก ที่มีสเตต (stateful) ใหม่โดยไม่มี key แต่ละ Element จะเก็บสเตตของผู้ที่เคยครองตำแหน่งนั้นไว้: ช่องกาถูกกาผิดที่ ช่องกรอกข้อความสลับกัน Key เปลี่ยนกฎการจับคู่: อัลกอริทึมจะค้นหา Element ที่พก key เดียวกัน แม้จะถูกย้ายที่ไปในรายการก็ตาม จงใช้ ValueKey กับตัวระบุทางธุรกิจที่คงที่ GlobalKey ไปไกลกว่านั้น มันช่วยให้คุณ ย้าย subtree ทั้งก้อนไปอยู่ใต้ parent ใหม่ (reparent) ได้โดยไม่สูญเสียสเตต แต่มันมีต้นทุนจริง (registry แบบ global, ข้อจำกัดว่าต้องไม่ซ้ำกันทั้ง tree) จงสงวนไว้ใช้กับความจำเป็นจริง ๆ อย่าง Form หรือการ reparent เท่านั้น

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, เลย์เอาต์) และ raster (การแปลงเลเยอร์เป็นภาพแรสเตอร์) งบประมาณอยู่ที่ราว 16 ms ที่ 60 Hz อาการกระตุก (jank) ทางฝั่ง raster แก้ไม่ได้ด้วยการปรับ build ของคุณให้เหมาะสม เพราะมันมาจากต้นทุนของการวาด (เงา, saveLayer, ภาพที่ใหญ่เกินไป) จงเปิด Track widget builds เพื่อดูว่าใครกำลังสร้างใหม่ และ debugRepaintRainbowEnabled เพื่อมองเห็นบริเวณที่ถูกวาดซ้ำ เส้นขอบจะเปลี่ยนสีในทุกครั้งที่มีการวาดซ้ำ RepaintBoundary แยก subtree ออกไปไว้ในเลเยอร์ของตัวเอง: spinner ที่วาดซ้ำอย่างต่อเนื่องจะไม่ทำให้เพื่อนบ้านต้องวาดใหม่ตามอีกต่อไป

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(),
          ),
        ),
      ),
    );
  }
}
overlay ของเฟรม + repaint rainbow: คู่หูสำหรับการวินิจฉัย

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ทำไมวิดเจ็ตแบบ const จึงถูกมองข้ามเมื่อ parent ของมันสร้างใหม่?
    • อินสแตนซ์เป็นแบบมาตรฐาน (canonical): identical เป็นจริง และ Element.updateChild ลัดวงจรข้ามทั้ง subtree
    • คอมไพเลอร์นำวิดเจ็ตออกจาก tree ในโหมด release
    • วิดเจ็ตแบบ const ไม่มีเมท็อด build
  2. คุณจัดเรียงรายการของวิดเจ็ตที่มีสเตตใหม่โดยไม่มี key จะเกิดอะไรขึ้น?
    • Element ถูกจับคู่ตามตำแหน่ง: แต่ละสมาชิกรับสืบทอดสเตตของผู้ที่เคยครองตำแหน่งนั้น
    • Flutter โยน exception ในโหมด debug
    • สเตตติดตามวิดเจ็ตที่ถูกย้ายแต่ละตัวไปโดยอัตโนมัติ
    • รายการปฏิเสธที่จะสร้างใหม่
  3. กราฟทั้งสองของ performance overlay วัดอะไร?
    • เวลาต่อเฟรมของเธรด UI (โค้ด Dart) และเธรด raster (การเรนเดอร์เลเยอร์)
    • หน่วยความจำที่ถูกจองและจำนวนวิดเจ็ต
    • เวลาในการ build และเวลาในการเก็บขยะ (garbage collection)