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) ว่า "แก้แค่ ผนังห้องนี้พอ" ไม่ต้องรื้อทั้งหลัง
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 เหมือนเดิม
ไหม ถ้าเหมือนไม่ต้องปลุกฉัน"
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) อยากกินจริงๆ ตอนนั้น
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
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.