Skip to main content

Performance

Reconciliation — React อัปเดต DOM ยังไง​

React ไม่ได้ "เขียนทับ" DOM ทั้งหมดทุกครั้งที่ state เปลี่ยน — มันสร้าง virtual DOM ใหม่ (tree ของ object ธรรมดา) แล้ว diff กับ virtual DOM เวอร์ชันก่อนหน้า หาเฉพาะส่วนที่ต่าง แล้วค่อยอัปเดต real DOM เท่าที่จำเป็น เท่านั้น

State เปลี่ยน → สร้าง virtual DOM ใหม่ → diff กับของเก่า → หา "ส่วนต่าง" →
อัปเดต real DOM เฉพาะส่วนนั้น (ไม่ใช่ทั้งหน้า)
🧠 จำง่ายๆ

"Virtual DOM = ร่างแบบก่อนส่งช่าง" — แทนที่จะรื้อบ้านทั้งหลังทุกครั้งที่อยากเปลี่ยนอะไรนิดหน่อย React วาดแบบใหม่บนกระดาษก่อน (virtual DOM) เทียบกับแบบเก่า แล้วบอกช่าง (browser) ว่า "แก้แค่ ผนังห้องนี้พอ" ไม่ต้องรื้อทั้งหลัง

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Diffing algorithm ของ React เป็น heuristic O(n) ไม่ใช่ algorithm diff ที่ "ถูกต้องที่สุด ตามทฤษฎี" (ซึ่งจะเป็น O(n³) ช้าเกินใช้งานจริง) มันตั้งอยู่บน 2 สมมติฐาน ที่ React ยอมรับว่า "ไม่สมบูรณ์แบบแต่พอเพียง": (1) element คนละ type (เช่น <div> เทียบ <span>) ถือว่าเป็นคนละ tree ไปเลย ไม่พยายาม diff ลึกลงไปข้างใน (ทำลายทิ้งทั้ง subtree แล้วสร้างใหม่) (2) element ใน list ต้องมี stable key ถึงจะ match กันถูกต้องข้าม render (ตามที่กล่าวในหัวข้อ List & Key) — การรู้ว่า diffing เป็นแค่ heuristic ที่ "เดาอย่างฉลาด" ไม่ใช่ algorithm ที่ perfect ช่วยอธิบาย ได้ว่าทำไมการเปลี่ยน element type บ่อยๆ (เช่น toggle ระหว่าง <div>/<section> ที่เนื้อหา เหมือนกัน) ถึงทำให้ state ข้างในหายไปทุกครั้ง (unmount แล้ว mount ใหม่) ทั้งที่ "ดูเหมือน" ควร จะ diff ได้ง่ายๆ

React.memo​

const ExpensiveRow = React.memo(function Row({ data }) {
return <div>{/* render ที่หนัก */}</div>;
});

React.memo ทำให้ component skip re-render ถ้า props เหมือนเดิม (shallow compare) — เหมาะกับ component ลูกที่ render หนักและ parent re-render บ่อยโดยไม่จำเป็น

🧠 จำง่ายๆ

"React.memo = ป้าย 'อย่าปลุกถ้าไม่มีอะไรเปลี่ยน'" — ปกติลูก re-render ทุกครั้งที่พ่อ re-render (ไม่ว่า props จะเปลี่ยนจริงไหม) React.memo คือป้ายที่บอกว่า "เช็คก่อนนะว่า props เหมือนเดิม ไหม ถ้าเหมือนไม่ต้องปลุกฉัน"

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

React.memo ใช้ shallow comparison (เทียบ reference ของแต่ละ prop ไม่ลงลึกไปข้างใน object) ซึ่งหมายความว่าถ้า parent ส่ง object/array/function literal ใหม่ทุกครั้งที่ render (เช่น <Row style={{ color: 'red' }} /> หรือ <Row onClick={() => {...}} />) React.memo จะไม่มีผลอะไรเลย เพราะ reference ของ object/function literal เหล่านี้ "ใหม่" เสมอทุก render แม้ค่าข้างในจะเหมือนเดิมทุกประการ ต้องใช้ useMemo/useCallback ที่ parent เพื่อรักษา reference ให้คงที่ก่อน ถึงจะทำให้ React.memo ที่ลูก "เห็น" ว่า props ไม่เปลี่ยนจริงๆ — เป็น เหตุผลที่สองเทคนิคนี้ (memo ที่ลูก + useMemo/useCallback ที่พ่อ) ต้องใช้คู่กันเสมอ ใช้แค่ ตัวใดตัวหนึ่งมักไม่ได้ผลตามที่ตั้งใจ

Code Splitting & Suspense​

const HeavyChart = React.lazy(() => import('./HeavyChart'));

function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
);
}

React.lazy + Suspense แบ่ง bundle JavaScript ออกเป็นชิ้นย่อย โหลดเฉพาะตอนที่ component นั้นถูกใช้จริง (แทนที่จะโหลดทั้งแอปมาพร้อมกันตั้งแต่แรก)

🧠 จำง่ายๆ

"Code splitting = สั่งอาหารทีละจาน ไม่ใช่ยกมาทั้งหมดในครั้งเดียว" — แอปใหญ่ที่โหลดทุกอย่างมา พร้อมกันเหมือนยกอาหารทุกจานมาตั้งโต๊ะตั้งแต่แรก (ช้า เปลืองโดยไม่จำเป็น) code splitting สั่ง มาทีละจานตามที่ลูกค้า (user) อยากกินจริงๆ ตอนนั้น

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Suspense ไม่ได้ผูกกับ React.lazy เท่านั้น — มันคือ general mechanism สำหรับรอ "สิ่งที่ยังไม่พร้อม" ที่ React ตั้งใจออกแบบให้ใช้กับการโหลดข้อมูล (data fetching) ได้ด้วย ในอนาคต ไม่ใช่แค่โหลด JS chunk เฉยๆ — framework อย่าง Next.js (App Router) และ Relay ใช้ Suspense สำหรับ data fetching จริงแล้ว (component "throw" promise ที่ยังไม่ resolve แล้วให้ Suspense boundary ที่ใกล้ที่สุดจับไว้แสดง fallback จนกว่า promise จะ resolve) เป็น mental model ที่ต่างจาก useEffect + isLoading state แบบเดิมโดยสิ้นเชิง — รู้ pattern นี้ไว้จะเข้าใจทิศทางที่ React ecosystem กำลังมุ่งไป (Server Components + Suspense-based data fetching) ได้ลึกกว่าคนที่รู้จัก Suspense แค่ในบริบท React.lazy

English Summary

React diffs a virtual DOM against the previous version to patch only the changed parts of the real DOM, using an O(n) heuristic (not a theoretically perfect but slower algorithm) — two key assumptions are that different element types are treated as entirely different subtrees (destroy-and-rebuild, losing internal state) and that list items need stable keys to match correctly across renders. React.memo skips re-rendering when props are shallow-equal, but object/array/function literal props get new references every render, silently defeating memo — it must be paired with useMemo/useCallback on the parent to actually work. Code splitting via React.lazy + Suspense loads JS chunks on demand; Suspense itself is a general "wait for something not ready yet" mechanism that frameworks like Next.js App Router and Relay now use for data fetching too, not just lazy-loaded components — a meaningfully different mental model from the classic useEffect + isLoading pattern.