ข้ามไปยังเนื้อหา

The Render Pipeline

เวลาวาด frame หนึ่ง Flutter จะรัน pipeline ที่ตายตัว รู้จัก phase เหล่านี้แล้วคุณจะรู้ว่าเวลาไปหมดที่ไหนและควร optimize ตรงไหน

flowchart LR
  build["1. Build
(run build methods)"] --> layout["2. Layout
(constraints down, sizes up)"]
  layout --> paint["3. Paint
(record draw commands)"]
  paint --> composite["4. Composite
(assemble layers, to GPU)"]
render pipeline ต่อ frame
  1. Build — widget ที่ dirty rebuild ออกมาเป็นคำอธิบาย widget ที่อัปเดต
  2. Layout — pass แบบ constraints-ลงล่าง/size-ขึ้นบน คำนวณ size และตำแหน่งของ render object ทุกตัว
  3. Paint — render object แต่ละตัวบันทึกคำสั่งวาด (ยังไม่ใช่ pixel) ลงใน layer
  4. Composite — layer ถูกประกอบและส่งให้ GPU rasterize

budget แคบมาก: จะได้ 60fps คุณมีเวลา ~16ms ต่อ frame (~8ms ที่ 120Hz) jank คือ frame ที่พลาด budget นั้น และเกือบทุกครั้งมาจากงานเยอะเกินไปใน build หรือ layout

widget เป็น คำอธิบาย ที่เบาและ immutable object ที่ layout และ paint จริง ๆ คือ RenderObject และอยู่ใน tree ที่สามจากสาม tree ของ Flutter:

  • Widget tree — configuration ที่ immutable ของคุณ (rebuild บ่อย, ถูก)
  • Element tree — กาว mutable ที่เก็บ state และเชื่อม widget กับ render object
  • Render treeRenderObject ที่คำนวณ layout และ paint (แพง; ถูกเก็บและ mutate ไม่ใช่ rebuild)

การแยกนี้คือ เหตุผล ที่ rebuild widget ถูก: widget ใหม่ถูกเทียบกับตัวเก่า แล้ว Flutter mutate render object เดิมในที่เดิมแทนที่จะสร้างส่วนที่แพงขึ้นใหม่

โดย default เมื่อ render object repaint ก็ลากให้เพื่อนบ้าน repaint ตามไปด้วย — เพราะแชร์ layer กัน RepaintBoundary เอา subtree ไปไว้บน layer ของตัวเอง เพื่อให้ repaint ของตัวเองไม่กระเพื่อมออกไปข้างนอก (และกลับกัน) นี่คือเครื่องมือหลักในการแยกพื้นที่ที่ animate หรือ repaint บ่อย

RepaintBoundary(
child: SpinningLogo(), // repaint บน layer ของตัวเอง ไม่ใช่ทั้งหน้าจอ
)

ใช้ครอบสิ่งที่ repaint บ่อย (animation, progress spinner) เพื่อไม่ให้ทั้ง frame ต้องบันทึกใหม่ ใช้มากไปเปลือง memory (แต่ละ boundary คือ layer) ดังนั้นใช้ตรงที่ profiling บอกว่ามี repaint churn

Flutter ยังสร้าง relayout boundary อัตโนมัติตรงที่ size ของ subtree เปลี่ยน parent ไม่ได้ (เช่น ภายใต้ tight constraints) — เพื่อให้การเปลี่ยนแปลงข้างในไม่บังคับให้ทั้ง tree ต้อง layout ใหม่ คุณแทบไม่ต้องจัดการเองด้วยมือ แต่เป็นเหตุผลที่การเปลี่ยนแปลงเฉพาะจุดยังคงอยู่เฉพาะจุด

จะเห็นทั้งหมดนี้ ใช้ Flutter DevTools: performance view โชว์เวลา build/layout/paint ต่อ frame และเครื่องมือ “Highlight repaints” กับ “Track widget rebuilds” โชว์ว่าอะไรกำลัง churn optimize สิ่งที่คุณวัด ไม่ใช่สิ่งที่คุณเดา

ลำดับของ render pipeline ต่อ frame คืออะไร?
ทำไม rebuild widget ถึงถูก?
RepaintBoundary ทำอะไร?
jank มักมาจากไหน?