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

Rendering Performance

component จะ re-render ด้วยเหตุผล 3 อย่างเท่านั้น:

  1. state ของตัวเองเปลี่ยน (มีการเรียก setter)
  2. parent re-render — โดย default เมื่อ parent render React จะ re-render children ทั้งหมด
  3. context ที่ component consume อยู่เปลี่ยนค่า

ข้อสองมักทำให้คนแปลกใจ: parent ที่ re-render จะ re-render ทั้ง subtree ข้างล่าง แม้ children ที่ props ไม่ได้เปลี่ยน React ทำแบบนี้เพราะไม่รู้ล่วงหน้าโดยไม่เช็ค ว่า output ของ child ขึ้นกับสิ่งที่เปลี่ยนหรือไม่

flowchart TB
  app["App re-render
(state เปลี่ยน)"] --> header["Header re-render"]
  app --> main["Main re-render"]
  main --> list["List re-render"]
  list --> item1["Item re-render"]
  list --> item2["Item re-render"]
  note["ทุก descendant re-render
แม้ props ไม่เปลี่ยน"]
render หนึ่งครั้ง propagate ลงทั้ง subtree

การ re-render ไม่ฟรี แต่ปกติแล้วถูก — จำไว้ว่า reconciliation ให้ผลเป็น zero DOM change เมื่อ output ไม่เปลี่ยน ปัญหาจะโผล่ก็ต่อเมื่อ subtree ขนาดใหญ่ หรือ component ที่หนัก re-render โดยไม่จำเป็นและบ่อย ๆ

วิธีแก้ที่ดีที่สุดคือเชิงโครงสร้าง ถ้า state อยู่สูงใน tree แต่มีแค่ branch เล็ก ๆ ที่ใช้ ทุกครั้งที่ state เปลี่ยนจะ re-render ทั้ง tree ย้าย state ลงมา ไปที่ component ที่ใช้จริง เพื่อให้การเปลี่ยนแปลง re-render แค่ branch เล็ก ๆ นั้น

// ❌ Search text อยู่ใน App ดังนั้นการพิมพ์ re-render ทั้ง app
function App() {
const [query, setQuery] = useState("");
return (
<>
<SearchBox query={query} onChange={setQuery} />
<HugeExpensiveDashboard /> {/* re-render ทุก keystroke! */}
</>
);
}
// ✅ ย้าย state เข้าไปใน SearchBox; การพิมพ์ re-render แค่ SearchBox
function App() {
return (
<>
<SearchBox />
<HugeExpensiveDashboard /> {/* ไม่ re-render ระหว่างพิมพ์ */}
</>
);
}

อีกทริกที่เกี่ยวกัน: ยก content ด้วย composition ถ้า parent ที่เปลี่ยนบ่อยต้องห่อ child ที่ช้า ให้ส่ง child ที่ช้าเข้าไปเป็น children — children ที่ส่งเป็น props จะไม่ re-render เพียงเพราะ state ของ parent เปลี่ยน

เมื่อย้าย state ไม่ได้และ child หนักจริง ให้ห่อ child ด้วย React.memo component ที่ memoize แล้วจะข้ามการ re-render เมื่อ props shallow-equal กับ render ก่อนหน้า — component นั้นกลายเป็นกำแพงที่ parent re-render ข้ามไม่ได้

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
// re-render เฉพาะเมื่อ `items` (โดย reference) เปลี่ยนจริง
return items.map((it) => <Row key={it.id} item={it} />);
});

จุดที่ต้องระวัง: React.memo ช่วยได้ก็ต่อเมื่อ props เสถียรจริง ๆ ส่ง object, array หรือ function แบบ inline ใหม่เป็น prop ทีไร shallow comparison จะ fail เสมอ — นั่นคือเหตุผลที่ memoization มักต้องใช้ useMemo/useCallback กับ props ด้วย (บทถัดไป)

อย่าเดาว่าความช้าอยู่ที่ไหน React DevTools Profiler บันทึก session และแสดงให้เห็นชัดว่า component ไหน render, บ่อยแค่ไหน, ใช้เวลาเท่าไร — และ ทำไม แต่ละตัวถึง render optimize component ที่ profiler ชี้ ไม่ใช่ตัวที่คุณสงสัย

เหตุผล 3 อย่างที่ component re-render คืออะไร?
เมื่อ parent re-render จะเกิดอะไรกับ children โดย default?
วิธีแก้ที่ดีที่สุด "ตัวแรก" สำหรับ component ที่ re-render บ่อยเกินไปคืออะไร?
`React.memo` ป้องกันการ re-render ได้จริงเมื่อไร?