กำจัดการสร้างใหม่ที่ไร้ประโยชน์ด้วย const, key และการปรับให้ตรงกัน (reconciliation) แล้ววัดผลด้วย DevTools
เปิดบทเรียนนี้ใน KodokonFlutter จัดการกับ tree สามต้น: วิดเจ็ต (การตั้งค่าที่เปลี่ยนแปลงไม่ได้และทิ้งได้), Element (อินสแตนซ์ที่มีชีวิตซึ่งพกพาสเตตไว้) และ render object (เลย์เอาต์, การวาด) การสร้างวิดเจ็ตใหม่นั้นราคาถูก สิ่งที่แพงคือเลย์เอาต์และการวาดที่ลามต่อกันไป ระหว่างการสร้างใหม่ Element.updateChild จะใช้กฎสามข้อตามลำดับ: หากวิดเจ็ตใหม่ เหมือนกันเป๊ะ (อินสแตนซ์เดียวกัน) กับตัวเก่า ทั้ง subtree จะถูกลัดวงจรข้ามไป หาก Widget.canUpdate เป็นจริง (runtimeType เดียวกัน, key เดียวกัน) Element จะถูกเก็บไว้และเพียงแค่ตั้งค่าใหม่ ไม่เช่นนั้น Element จะถูกทำลายแล้วสร้างขึ้นมาใหม่ (re-inflate) รวมถึงสเตตด้วย คำสงวน const ใช้ประโยชน์จากกฎข้อแรก: นิพจน์คงที่จะถูก ทำให้เป็นรูปแบบมาตรฐาน (canonicalize) โดยคอมไพเลอร์ อินสแตนซ์เดิมจะกลับมาในทุกการ build ทำให้ identical เป็นจริง แล้วทั้งกิ่งจึงถูกข้ามไป
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');
}
}สำหรับรายการของ children การปรับให้ตรงกันจะจับคู่ตาม ตำแหน่ง ก่อน หากคุณจัดเรียงสมาชิก ที่มีสเตต (stateful) ใหม่โดยไม่มี key แต่ละ Element จะเก็บสเตตของผู้ที่เคยครองตำแหน่งนั้นไว้: ช่องกาถูกกาผิดที่ ช่องกรอกข้อความสลับกัน Key เปลี่ยนกฎการจับคู่: อัลกอริทึมจะค้นหา Element ที่พก key เดียวกัน แม้จะถูกย้ายที่ไปในรายการก็ตาม จงใช้ ValueKey กับตัวระบุทางธุรกิจที่คงที่ GlobalKey ไปไกลกว่านั้น มันช่วยให้คุณ ย้าย subtree ทั้งก้อนไปอยู่ใต้ parent ใหม่ (reparent) ได้โดยไม่สูญเสียสเตต แต่มันมีต้นทุนจริง (registry แบบ global, ข้อจำกัดว่าต้องไม่ซ้ำกันทั้ง tree) จงสงวนไว้ใช้กับความจำเป็นจริง ๆ อย่าง Form หรือการ reparent เท่านั้น
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)),
),
],
);
}
}จงวัดผลก่อนปรับให้เหมาะสม ใน DevTools มุมมอง Performance จะติดตามเวลาต่อเฟรมของสองเธรดที่สำคัญ: UI (โค้ด Dart ของคุณ: build, เลย์เอาต์) และ raster (การแปลงเลเยอร์เป็นภาพแรสเตอร์) งบประมาณอยู่ที่ราว 16 ms ที่ 60 Hz อาการกระตุก (jank) ทางฝั่ง raster แก้ไม่ได้ด้วยการปรับ build ของคุณให้เหมาะสม เพราะมันมาจากต้นทุนของการวาด (เงา, saveLayer, ภาพที่ใหญ่เกินไป) จงเปิด Track widget builds เพื่อดูว่าใครกำลังสร้างใหม่ และ debugRepaintRainbowEnabled เพื่อมองเห็นบริเวณที่ถูกวาดซ้ำ เส้นขอบจะเปลี่ยนสีในทุกครั้งที่มีการวาดซ้ำ RepaintBoundary แยก subtree ออกไปไว้ในเลเยอร์ของตัวเอง: spinner ที่วาดซ้ำอย่างต่อเนื่องจะไม่ทำให้เพื่อนบ้านต้องวาดใหม่ตามอีกต่อไป
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(),
),
),
),
);
}
}