Rendering Performance
ทำไม component ถึง re-render
หัวข้อที่มีชื่อว่า “ทำไม component ถึง re-render”component จะ re-render ด้วยเหตุผล 3 อย่างเท่านั้น:
- state ของตัวเองเปลี่ยน (มีการเรียก setter)
- parent re-render — โดย default เมื่อ parent render React จะ re-render children ทั้งหมด
- 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 ไม่เปลี่ยน"]
การ re-render ไม่ฟรี แต่ปกติแล้วถูก — จำไว้ว่า reconciliation ให้ผลเป็น zero DOM change เมื่อ output ไม่เปลี่ยน ปัญหาจะโผล่ก็ต่อเมื่อ subtree ขนาดใหญ่ หรือ component ที่หนัก re-render โดยไม่จำเป็นและบ่อย ๆ
แก้ #1: เก็บ state ให้ต่ำและ local
หัวข้อที่มีชื่อว่า “แก้ #1: เก็บ state ให้ต่ำและ local”วิธีแก้ที่ดีที่สุดคือเชิงโครงสร้าง ถ้า state อยู่สูงใน tree แต่มีแค่ branch เล็ก ๆ ที่ใช้ ทุกครั้งที่ state เปลี่ยนจะ re-render ทั้ง tree ย้าย state ลงมา ไปที่ component ที่ใช้จริง เพื่อให้การเปลี่ยนแปลง re-render แค่ branch เล็ก ๆ นั้น
// ❌ Search text อยู่ใน App ดังนั้นการพิมพ์ re-render ทั้ง appfunction App() { const [query, setQuery] = useState(""); return ( <> <SearchBox query={query} onChange={setQuery} /> <HugeExpensiveDashboard /> {/* re-render ทุก keystroke! */} </> );}
// ✅ ย้าย state เข้าไปใน SearchBox; การพิมพ์ re-render แค่ SearchBoxfunction App() { return ( <> <SearchBox /> <HugeExpensiveDashboard /> {/* ไม่ re-render ระหว่างพิมพ์ */} </> );}อีกทริกที่เกี่ยวกัน: ยก content ด้วย composition ถ้า parent ที่เปลี่ยนบ่อยต้องห่อ child ที่ช้า ให้ส่ง child ที่ช้าเข้าไปเป็น children — children ที่ส่งเป็น props จะไม่ re-render เพียงเพราะ state ของ parent เปลี่ยน
แก้ #2: หยุด propagation ด้วย React.memo
หัวข้อที่มีชื่อว่า “แก้ #2: หยุด propagation ด้วย React.memo”เมื่อย้าย 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 ด้วย (บทถัดไป)
วัดผลก่อน: Profiler
หัวข้อที่มีชื่อว่า “วัดผลก่อน: Profiler”อย่าเดาว่าความช้าอยู่ที่ไหน React DevTools Profiler บันทึก session และแสดงให้เห็นชัดว่า component ไหน render, บ่อยแค่ไหน, ใช้เวลาเท่าไร — และ ทำไม แต่ละตัวถึง render optimize component ที่ profiler ชี้ ไม่ใช่ตัวที่คุณสงสัย