โหมดมืด
บทที่ 10 — Animation + Performance
🔴 ระดับ: สูง — อ่านทีหลังได้ เรื่อง performance ส่วนใหญ่ค่อยมาสนใจตอนแอปเริ่มช้าจริง ๆ มือใหม่ไม่ต้องรีบ
สิ่งที่ควรรู้ก่อน (Prerequisite): บทที่ 2 (
useMemo/useCallback/useTransition), บทที่ 9 (Suspense + lazy)
📖 คำศัพท์รวมของบท:
- animation = การเคลื่อนไหวของ UI (ปุ่มกระเด้ง, list slide-in)
- performance = แอปเร็ว/ลื่นแค่ไหน
- profiling = วัดด้วยเครื่องมือว่าจุดไหนช้า (React Profiler, Chrome DevTools)
- memoization = จำผลลัพธ์เก่า ไม่คำนวณซ้ำ (
useMemo,useCallback,React.memo)- code splitting = แตก bundle เป็นชิ้นเล็ก โหลดเฉพาะที่ใช้
- bundle = ไฟล์ JS รวมที่ส่งให้ browser (ยิ่งใหญ่ยิ่งโหลดช้า)
- tree-shake = bundler ตัดโค้ดที่ไม่ได้ใช้ออกตอน build
- lazy load = โหลดเฉพาะตอนเรียก (ไม่โหลดตั้งแต่แรก)
- Web Vitals = ตัวชี้วัดประสิทธิภาพเว็บมาตรฐาน Google
- LCP (Largest Contentful Paint) = เวลาที่ content หลักโผล่ (< 2.5s ดี)
- INP (Interaction to Next Paint) = เวลาที่ใช้ตั้งแต่ user กดจนเห็น UI ตอบสนอง (แทน FID ตั้งแต่ มี.ค. 2024, < 200ms ดี)
- CLS (Cumulative Layout Shift) = layout เด้งระหว่าง load (< 0.1 ดี)
- TTFB (Time to First Byte) = เวลาที่ server ส่ง byte แรกกลับมา
- concurrent feature = React 18+ feature ที่ "interrupt ได้" — แยก urgent vs non-urgent update (
useTransition,useDeferredValue)- main thread = thread เดียวของ browser ที่รัน JS + วาด UI → ถ้า JS run นาน UI ค้าง
- long task = งานที่ main thread ใช้เวลารันเกิน 50ms รวด — ระหว่างนั้น browser ตอบสนอง event อื่นไม่ได้ (ปุ่มค้าง, พิมพ์ไม่ขึ้น)
บทนี้ 2 หัวข้อ:
- Animation — ทำให้ UI ลื่นไหล (Framer Motion ⭐)
- Performance — ครอบคลุม 4 เรื่อง:
- profiling (วัดหาจุดช้า)
- memoization (จำผลลัพธ์ไว้ไม่คำนวณซ้ำ)
- code splitting (แตกโค้ดเป็นชิ้นโหลดเท่าที่ใช้)
- bundle size (ขนาดไฟล์รวมที่ส่งให้ browser)
หลังจบบท คุณจะ:
- ทำ animation ด้วย Framer Motion (Motion ปี 2026)
- หา performance bottleneck ด้วย React Profiler + Chrome DevTools
- เข้าใจ React 19 compiler — ทำไม
useMemoลดลง - Optimize bundle size + initial load (Web Vitals)
Part 1: Animation
1. ทำ animation มีกี่วิธี
| วิธี | ดี | ไม่ดี |
|---|---|---|
| CSS Transition | เร็ว, simple | จำกัด trigger (hover, focus) |
| CSS Animation (@keyframes) | declarative | ยากเมื่อ state-based |
react-transition-group | classic | API เก่า |
| Framer Motion / Motion ⭐ | declarative¹, smooth (ลื่น), gesture² | bundle ขนาดกลาง |
| react-spring | physics-based, smooth | learning curve (ใช้เวลาทำความเข้าใจนานกว่า) |
| GSAP | powerful (timeline) | imperative (เชิงสั่งการ — บอกทีละขั้นว่าให้ทำอะไร) |
| Auto Animate | one-liner (ใช้งานได้ด้วยโค้ดบรรทัดเดียว) | จำกัด case |
¹ declarative = เชิงประกาศ — บอกว่าต้องการผลลัพธ์อะไร ไม่ต้องสั่งทีละขั้น ² gesture = รองรับท่ามือถือ เช่น drag (ลาก) / swipe (ปัด)
ปี 2026: Framer Motion (rebrand เป็น Motion) เป็น standard
2. CSS Transition + Tailwind
ก่อนนึกถึง animation library ให้เริ่มจาก CSS transition ก่อน — micro-interaction ส่วนใหญ่ (hover scale, fade, color) ทำได้ด้วย Tailwind class (transition-all duration-200) ซึ่งเบาและ performant อย่ารีบหยิบ library กับงานง่าย ๆ:
tsx
<button className="
bg-blue-500 hover:bg-blue-600
scale-100 hover:scale-105
transition-all duration-200 ease-out
">
Click me
</button>90% ของ micro-interaction (การโต้ตอบเล็ก ๆ เช่น hover (เอาเมาส์ชี้), click feedback (การตอบสนองเมื่อกด), ปุ่มเด้ง) ใช้แค่นี้ — อย่ารีบไปใช้ library
3. Motion / Framer Motion
bash
npm install motion📦 ชื่อ package: เดิมชื่อ
framer-motionปี 2024 rebrand เป็นmotion— ตัวอย่างเก่าที่หาใน Google/StackOverflow ยังเห็นimport ... from 'framer-motion'อยู่ ใช้ตัวใหม่motionแทนได้เลย
⚠️ import path:
'motion/react'ใช้กับnpm install motionเท่านั้น — ถ้า project ยังใช้framer-motionเก่า ให้ import จาก'framer-motion'แทน
motion.div คือ <div> ที่ Motion ห่อไว้เพื่อให้รับ props animation พิเศษได้ เขียน motion. หน้า HTML element ใดก็ได้ เช่น motion.button, motion.ul, motion.img
tsx
import { motion } from 'motion/react';
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Fade in + slide up
</motion.div>API หลัก:
initial— state เริ่มต้นanimate— state ปลายทางexit— state ตอน unmount (ใช้กับ AnimatePresence)transition— duration, ease, delaywhileHover,whileTap,whileInView
4. AnimatePresence — animate ตอน unmount
ปัญหาคลาสสิก: animate ตอน element ปรากฏง่าย แต่ตอนหายไป (unmount) React เอาออกทันทีไม่ทันได้ animate — AnimatePresence แก้โดยรอให้ exit animation จบก่อนค่อย remove (ลบออกจาก DOM จริง) สำคัญมากสำหรับ modal/toast/dropdown:
tsx
import { motion, AnimatePresence } from 'motion/react';
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>→ component ถูก unmount → AnimatePresence รอ exit animation จบก่อน remove
5. Stagger Children
stagger คือการ animate ลูก ๆ ทยอยกันทีละนิดแทนที่จะพร้อมกัน — ตั้ง staggerChildren ที่ parent แล้วลูกแต่ละตัวจะเริ่ม animation เหลื่อมเวลากัน ให้ความรู้สึกเนี้ยบลื่นไหล (polished) เห็นบ่อยใน landing page ตอน list ค่อย ๆ โผล่:
variants คือการตั้งชื่อ animation state ไว้ล่วงหน้าเป็น object แล้วอ้างถึงด้วยชื่อ (เช่น "hidden", "show") แทนที่จะ pass object ตรง ๆ ทุกครั้ง — ช่วยให้ parent กับ child ใช้ animation state เดียวกันได้โดยอัตโนมัติ (stagger)
tsx
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 } // ทยอย 0.1 วินาที
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
<motion.ul variants={container} initial="hidden" animate="show">
{users.map(u => (
<motion.li key={u.id} variants={item}>
{u.name}
</motion.li>
))}
</motion.ul>ทำ list animate ทีละตัวเร็ว ๆ — เห็นใน landing page เยอะ
6. Scroll-triggered Animation
animate ตอน element เลื่อนเข้ามาในจอ (scroll into view) — ใช้ whileInView แทน animate แล้ว Motion จะ trigger เมื่อ element ปรากฏ ตั้ง viewport={{ once: true }} ให้ animate แค่ครั้งแรก (ไม่ซ้ำเมื่อ scroll กลับมา):
tsx
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} // animate แค่ครั้งแรก
transition={{ duration: 0.6 }}
>
เห็นแล้ว animate
</motion.div>7. Gestures — Drag
Motion รองรับ gesture เช่น drag ในตัว — ใส่ prop drag ก็ลากได้, dragConstraints จำกัดขอบเขต, dragElastic ให้ความรู้สึกยืดหยุ่น ทำ UI แบบ swipe/reorder/slider ได้โดยไม่ต้องจัดการ pointer event เอง:
tsx
<motion.div
drag // drag ทุกทาง
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.2}
whileTap={{ scale: 0.95 }}
>
ลากฉัน
</motion.div>8. Layout Animation ⚡
ฟีเจอร์ทรงพลังที่สุดของ Motion — แค่ใส่ prop layout แล้วเมื่อขนาด/ตำแหน่งของ element เปลี่ยน Motion animate transition ให้อัตโนมัติ (เช่น list reorder = สลับลำดับ, accordion expand = ขยาย)
ส่วน layoutId เดียวกันข้าม component ทำ shared element transition (animation ที่ element เดิมดูเหมือน "ลอย" ข้ามหน้าหรือข้าม component — เช่น click thumbnail แล้ว expand เป็นรูปเต็ม):
tsx
<motion.div layout>
{/* ขนาด/ตำแหน่งเปลี่ยน → animate อัตโนมัติ */}
</motion.div>
// หรือ shared layout
<motion.div layoutId="hero" /> // ใน list
<motion.div layoutId="hero" /> // ใน detail page — animate transitionlayout prop = "ถ้า layout เปลี่ยน → animate" — ใช้ทำ list reorder, accordion expand
9. ตัวอย่างจริง — Animated Modal
ตัวอย่างที่รวมทุกเทคนิค — Animated Modal ที่ใช้ AnimatePresence (animate เข้า/ออก), backdrop fade, content scale และ Portal เห็นได้ว่า animation ที่ดูดีในงานจริงประกอบจาก primitive หลายตัวที่เรียนมา:
tsx
function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black/50 z-40"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg z-50"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}10. ทางเลือก: GSAP
GSAP = imperative animation engine (เครื่องยนต์ animation เชิงสั่งการ — บอกทีละขั้นว่าให้ทำอะไร) ตรงข้ามกับ Motion ที่เป็น declarative (บอกแค่ว่าต้องการผลลัพธ์อะไร) — GSAP ดีสำหรับ timeline (ลำดับเหตุการณ์) ซับซ้อน
bash
npm install gsap @gsap/reacttsx
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
function HeroSection() {
const container = useRef<HTMLDivElement>(null);
useGSAP(() => {
gsap.timeline()
.from('.title', { y: 50, opacity: 0, duration: 1 })
.from('.subtitle', { y: 30, opacity: 0, duration: 0.8 }, '-=0.5')
.from('.cta', { scale: 0, duration: 0.5, ease: 'back.out' }, '-=0.3');
}, { scope: container });
return (
<div ref={container}>
<h1 className="title">Hello</h1>
<p className="subtitle">Welcome</p>
<button className="cta">Start</button>
</div>
);
}Motion vs GSAP:
- Motion = ดีสำหรับ component-level
- GSAP = ดีสำหรับ landing page / hero / complex timeline
Part 2: Performance
11. กฎทอง — อย่า optimize ก่อน
"การ optimize ก่อนเวลาอันควร คือต้นตอของความเลวร้ายทั้งปวง" — Donald Knuth
("Premature optimization is the root of all evil." — อย่าเพิ่งรีบจูนให้เร็วถ้ายังไม่รู้ว่าตรงไหนช้าจริง)
ทำตามลำดับ:
- เขียน code ปกติ
- วัด ก่อน (Profiler, Web Vitals)
- หา bottleneck จริง
- แก้เฉพาะที่ช้าจริง
อย่าเริ่มด้วยการใส่ useMemo ทุกที่ — มันมี cost ของตัวมัน
12. React Profiler
ใน Chrome DevTools → Profiler tab (ติด React DevTools extension)
- กด record
- ทำ interaction
- กด stop
- ดู:
- Component ไหน render
- แต่ละ render ใช้เวลาเท่าไหร่
- ทำไม render (props เปลี่ยน, state เปลี่ยน, parent render)
→ หา component ที่ render บ่อย/นานเกิน
13. ใช้ Profiler API ในโค้ด
นอกจาก Profiler ใน DevTools React ยังมี <Profiler> component ที่วัด render ในโค้ดได้ — ครอบส่วนที่อยากวัดแล้วรับ callback ที่บอก phase และเวลา (actualDuration) มีประโยชน์ตอนอยาก log performance ใน production หรือทำ automated perf test:
tsx
import { Profiler, type ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
console.log(`${id} (${phase}): ${actualDuration}ms`);
};
<Profiler id="UserList" onRender={onRender}>
<UserList />
</Profiler>→ ส่งไป analytics / log เพื่อ track performance ใน production
14. React 19 Compiler — เปลี่ยนเกม (game changer)
⚠️ สถานะปี 2026: React Compiler ยังเป็น RC (Release Candidate, ยังไม่ stable v1.0) ต้องลง
babel-plugin-react-compilerแยก — ยังไม่ได้ bundle มากับ React 19 core ตามแผน roadmap ใน ปี 2025-2026 จะค่อยๆ stable แต่ตอนนี้ยัง opt-in อยู่ ดู react.dev/learn/react-compiler ก่อนเปิดใช้
ก่อน React 19 — ต้อง useMemo, useCallback, React.memo เอง:
tsx
const result = useMemo(() => expensive(deps), [deps]);
const handleClick = useCallback(() => {...}, [deps]);
const MemoComponent = React.memo(Component);React 19 Compiler = compile time → ทำให้ memoization อัตโนมัติ
tsx
// React 19 compiler — เหมือนเดิม แต่ภายในมี memoization auto
function Component({ items }: { items: Item[] }) {
const filtered = items.filter(x => x.active); // compiler optimize ให้
const handleClick = () => {/* ... */};
return <List items={filtered} onClick={handleClick} />;
}ถ้า project เปิด React Compiler (RC — opt-in) — เกือบไม่ต้องใส่ useMemo/useCallback เอง Compiler ครอบคลุม case ส่วนใหญ่ — เหลือไว้ก็แต่ measured hotspot (จุดที่ช้าจริง ๆ ที่วัดแล้วด้วย Profiler) ที่ profile แล้วเห็นว่า Compiler ไม่ครอบคลุม
📝 หมายเหตุ: Compiler ต้องการ code ที่ follow Rules of React (ดู §14 ด้านล่าง) จึงจะ optimize ได้ครบ — component ที่ mutate state ตรง ๆ หรือทำ side effect ใน render จะไม่ถูก optimize
enable React Compiler
Vite (Babel-based):
bash
npm i -D babel-plugin-react-compilerts
// vite.config.ts
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react({
babel: {
plugins: [['babel-plugin-react-compiler']],
},
})],
});📝 หมายเหตุ: option ของ
babel-plugin-react-compilerอาจเปลี่ยนตามเวอร์ชัน — ดู config ที่ถูกต้องล่าสุดได้ที่ react.dev/learn/react-compiler
Next.js 15:
ts
// next.config.ts
const config = { experimental: { reactCompiler: true } };⚠️ Rules of React — ที่ต้องตามให้ Compiler ทำงาน
- Component + hook ต้อง pure (บริสุทธิ์ — output เดิมทุกครั้งที่รับ input เดิม) — ไม่มี side effect (ผลข้างเคียง เช่น เรียก API, แก้ DOM, อ่าน/เขียนไฟล์) ใน body
- ห้าม mutate props/state/ref ตรง ๆ
- เรียก hook ตามลำดับเดียวกันทุก render
- ไม่ใช้ ref ภายใน render phase (ช่วงที่ component กำลังคำนวณ JSX — ref ต้องใช้ใน
useEffectหรือ event handler เท่านั้น)
→ ESLint plugin eslint-plugin-react-compiler warn ถ้าผิดกฎ — install ด้วย
15. ถ้ายังไม่ใช้ Compiler — Manual Memoization
🟢 อ่านก่อน: ถ้า project เปิด React Compiler (default ของ project ใหม่ปี 2026) คุณแทบไม่ต้องเขียนโค้ดในส่วนนี้เลย Compiler insert memoization ให้อัตโนมัติแล้ว ส่วนนี้ไว้สำหรับ:
- codebase เก่าที่ยังไม่ migrate ไป Compiler
- measured hotspot — จุดที่ Profiler บอกแล้วว่าช้าจริง ๆ และ Compiler จัดการไม่ครอบคลุม
- case ที่ต้อง custom equality (เช่น
React.memoพร้อม customarePropsEqual)อย่าเริ่มจาก "ใส่
useMemo/useCallbackทุกที่" — มันมี cost และทำให้ code อ่านยากโดยไม่ได้กำไร profile ก่อนเสมอ
React.memo
tsx
type ItemProps = { user: User; onClick?: () => void };
const Item = React.memo(function Item({ user, onClick }: ItemProps) {
console.log('Item render', user.id);
return <div onClick={onClick}>{user.name}</div>;
});React.memo skip re-render ถ้า props ไม่เปลี่ยน (shallow compare = เปรียบเทียบแค่ reference ของ value ไม่ได้เปรียบเทียบเนื้อหาข้างใน — ดังนั้น object ใหม่ที่เนื้อหาเหมือนกันก็ถือว่า "ต่างกัน" ในสายตาของ React.memo)
⚠️ Props ที่เป็น object / function → ต้อง memoize ด้วย:
tsx
// ❌ Anti-pattern 1: inline object ใหม่ทุก render → Item re-render
function ParentBad1() {
return <Item user={{ id: 1, name: 'Anna' }} />;
}
// ❌ Anti-pattern 2: inline function ใหม่ทุก render → Item re-render
function ParentBad2() {
return <Item user={{ id: 1, name: 'Anna' }} onClick={() => console.log('click')} />;
}
// ✅ แก้ด้วย useMemo + useCallback
function ParentGood() {
const user = useMemo(() => ({ id: 1, name: 'Anna' }), []);
const handleClick = useCallback(() => console.log('click'), []);
return <Item user={user} onClick={handleClick} />;
}useMemo / useCallback
tsx
// Expensive calculation — cache จนกว่า deps จะเปลี่ยน
// [...items] คัดลอก array ก่อน sort เพื่อไม่ mutate array ต้นฉบับ
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Function ที่จะ pass ให้ memoized child
const handleSelect = useCallback((id: number) => {
setSelectedId(id);
}, []);กฎ:
- ไม่ต้องใส่ทุกที่ — มี cost
- ใส่เมื่อ profiler บอกว่าจำเป็น
- ใส่เมื่อ pass function/object ให้ memoized child
16. Virtual List — สำหรับ list ใหญ่
10,000 row ใน DOM = ช้าเสมอ → ใช้ virtualization (render เฉพาะ row ที่เห็น)
Virtualization ทำงานอย่างไร: แทนที่จะสร้าง DOM element 10,000 ชิ้น library จะสร้างเฉพาะ element ที่อยู่ในพื้นที่ที่มองเห็น (viewport) และสลับ element เมื่อ scroll — เหมือนหน้าต่างที่เลื่อนดูกระดาษยาว แต่กระดาษที่โผล่ให้เห็นมีแค่ส่วนเล็ก ๆ เท่านั้น
bash
npm install @tanstack/react-virtualtsx
import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
function HugeList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // ความสูงของแต่ละ row (row height)
overscan: 5, // render row นอกจอ 5 row ทั้ง 2 ทิศ — กัน flicker (ภาพกระพริบ/ว่างชั่วครู่) ตอน scroll เร็ว
});
return (
<div ref={parentRef} className="h-96 overflow-auto">
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(v => (
<div
key={v.key}
style={{
position: 'absolute',
top: 0, left: 0,
transform: `translateY(${v.start}px)`,
height: v.size,
width: '100%',
}}
>
{items[v.index].name}
</div>
))}
</div>
</div>
);
}→ render เฉพาะ row ที่เห็น + overscan → smooth scroll 100k row ได้
17. Code Splitting (จากบทที่ 5)
หนึ่งใน performance win ที่ง่ายและคุ้มสุดคือ code splitting — แทนที่จะโหลด JS ทั้ง app ในครั้งแรก ใช้ lazy() + dynamic import แยก bundle ตามหน้า แล้ว bundler โหลด chunk เฉพาะตอนที่ผู้ใช้เข้าหน้านั้น ทำให้ first load เร็วขึ้นมาก
lazy() คือการบอก React ว่าให้โหลด component นี้เฉพาะตอนที่ต้องใช้จริง ไม่โหลดตั้งแต่เปิดแอป — ต้องใช้ร่วมกับ <Suspense> เพื่อแสดง fallback ระหว่างโหลด:
tsx
const Dashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>แยก bundle → first load เร็ว → โหลด chunk เมื่อใช้
18. Bundle Analysis
Plugin ใน Vite คือส่วนเสริมที่ขยายความสามารถของ bundler — rollup-plugin-visualizer คือ plugin ที่สร้างไฟล์ HTML แสดง tree map ขนาด bundle แต่ละส่วน ช่วยให้เห็นว่า dependency ไหนใหญ่เกินไปหรือไม่จำเป็น:
bash
npm install --save-dev rollup-plugin-visualizerts
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [
react(),
visualizer({ open: true, filename: 'bundle-stats.html' })
]bash
npm run build→ เปิด bundle-stats.html เห็น tree map ของ bundle
→ หา dependency ใหญ่ที่ไม่จำเป็น
19. Web Vitals — measurement หลัก
คำศัพท์: Web Vitals (ชุดตัวชี้วัดของ Google ที่บอกว่าเว็บโหลดเร็ว/ตอบสนองดี/ภาพนิ่งแค่ไหน), metric (ตัวเลขที่ใช้วัด)
| ตัวชี้วัด (Metric) | คืออะไร | เป้าหมาย |
|---|---|---|
| LCP (Largest Contentful Paint) | เวลาที่ content หลักโผล่ | < 2.5s |
| INP (Interaction to Next Paint) ⭐ | latency ของ user interaction (เปลี่ยนจาก FID มี.ค. 2024) | < 200ms |
| CLS (Cumulative Layout Shift) | layout เด้งระหว่าง load | < 0.1 |
| TTFB (Time to First Byte) | server response | < 600ms |
💡 INP สำคัญสุดสำหรับ React app — Google เปลี่ยนจาก FID เป็น INP ในเดือน มี.ค. 2024 เป็น Core Web Vital ตัวที่ 3 แล้ว INP วัดทุก interaction (ไม่ใช่แค่ครั้งแรก) → ทำให้ "form ที่กดส่งช้า" หรือ "click filter list 1000 รายการแล้วหน่วง" โดน flag ทันที
bash
npm install web-vitalstsx
// ต้องการ web-vitals v4+ (v3- ใช้ getLCP/getINP/getCLS)
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);ส่งไป analytics → track ใน production
19.1 ปรับ INP ใน React — useTransition + useDeferredValue
INP แย่บ่อยที่สุดเพราะ click ทำให้ main thread (เธรดหลักของ browser) ถูก block
ตัวอย่าง: click → state update → re-render tree ใหญ่ → block 300ms = ปุ่มหน่วง
แก้ด้วย concurrent feature ของ React 18+ (useTransition, useDeferredValue):
tsx
// ❌ INP แย่ — filter 10,000 รายการ block main thread
function SearchList({ items }: Props) {
const [q, setQ] = useState('');
const filtered = items.filter(i => i.name.includes(q)); // ช้า
return (
<>
<input value={q} onChange={e => setQ(e.target.value)} />
<List items={filtered} />
</>
);
}
// ✅ INP ดี — useDeferredValue แยก urgent (input) จาก non-urgent (filter)
// state เดียว — `q` ตามทันที, `deferredQ` ตามหลัง (React มี priority ต่ำ)
function SearchList({ items }: Props) {
const [q, setQ] = useState('');
const deferredQ = useDeferredValue(q); // ⭐ ตัวเดียวพอสำหรับ pattern นี้
const filtered = items.filter(i => i.name.includes(deferredQ));
return (
<>
<input value={q} onChange={e => setQ(e.target.value)} />
<List items={filtered} />
</>
);
}📝 เลือก
useTransitionเมื่อไหร่: ถ้าคุณเป็นเจ้าของ source state และมี หลาย setState ที่ต้องเข้า lane เดียวกัน หรืออยากได้isPendingindicator ให้ใช้startTransition(() => { ... })— แต่ถ้ามีแค่qตัวเดียวอย่างตัวอย่างนี้useDeferredValueสั้นกว่าและเป็น idiomatic กว่า (ไม่ต้องสร้าง state ซ้ำซ้อน)
Pattern อื่นที่ช่วย INP:
ใช้
useDeferredValueกับ prop ที่เปลี่ยนเร็ว (search query จาก parent)ใช้ virtual list เมื่อ render row > 100 (ดู §16)
ใช้
scheduler.yield()ใน long task (งานที่ใช้เวลาเกิน 50ms) เพื่อหยุดพักชั่วคราวให้ main thread จัดการ event อื่นได้ก่อน⚠️ Browser support:
scheduler.yield()ยังเป็น API ใหม่และรองรับเฉพาะบาง browser (Firefox/Safari ยังไม่รองรับเต็มที่ ณ ที่เขียนบทนี้) — เช็ค caniuse.com หรือ MDN เพื่อดูสถานะ browser support ล่าสุดก่อนใช้จริง เพราะ API ระดับนี้เปลี่ยนสถานะเร็ว ต้องเช็ค feature detection ก่อนใช้ใน production หรือใช้MessageChannel/setTimeout(0)เป็น fallbackหลีกเลี่ยง
scrollevent ที่ทำงานหนัก — ใช้IntersectionObserverแทน
20. Optimization Checklist
Code
- [ ] ใช้ React 19 (compiler) หรือ memoize ถูกที่
- [ ] Virtual list สำหรับ list > 100 row
- [ ] Lazy load route + heavy component
- [ ] Avoid inline object/function ใน memoized child
Assets
- [ ] Image lazy load (
loading="lazy") - [ ] Image format ใหม่ (WebP, AVIF)
- [ ] Responsive image (
srcset) - [ ] Font subset + preload
- [ ] Critical CSS inline
Network
- [ ] HTTP/2 หรือ HTTP/3
- [ ] CDN
- [ ] Cache headers
- [ ] gzip / brotli compression
Bundle
- [ ] Tree-shake ทำงาน (ESM, no side effects)
- [ ] Dynamic import for heavy lib (chart, date-fns)
- [ ] Drop unused deps
Server
- [ ] Server Components (Next.js) — ลด JS shipped
- [ ] SSR / SSG สำหรับ initial render
- [ ] Edge functions
21. ⚠️ Anti-patterns
| ❌ | ✅ |
|---|---|
ใส่ useMemo / useCallback ทุก variable | ใส่เฉพาะที่ profiler บอกว่าช้า |
ใช้ React.memo ทุก component | ใช้เฉพาะที่ parent re-render บ่อย + props stable |
| Optimize ก่อน measure | profile ก่อนเสมอ |
| Bundle ใหญ่ตอน first load | code split + lazy load |
| Render list 10k row ตรง ๆ | virtual list |
Inline <img> ใหญ่ ไม่มี lazy + responsive | loading="lazy" + srcset |
| Layout shift จาก image / font | reserve size + font-display |
22. Checkpoint
📝 หมายเหตุ: checkpoint บทนี้ ตั้งใจไม่มีเฉลย ให้ลองทำเองก่อน ถ้าติดให้ย้อนไปดูตัวอย่างโค้ดในบทแล้วดัดแปลง
🛠️ Checkpoint 10.1 — Page Transition
ใช้ Motion + React Router → animate ตอนเปลี่ยน route (fade + slide)
🛠️ Checkpoint 10.2 — Virtual List
รับ data 50,000 user → ทำ list ที่ scroll smooth + filter search
🛠️ Checkpoint 10.3 — Bundle Diet
- รัน
npm run buildดู bundle ขนาด - ใช้ visualizer หา dependency ใหญ่
- ลด: lazy load route, dynamic import chart lib, remove unused deps
- เปรียบเทียบขนาด bundle ก่อนและหลัง (before/after)
🛠️ Checkpoint 10.4 — Lighthouse
รัน Lighthouse ใน Chrome DevTools → ทำให้ทุก metric > 90 (Performance, Accessibility, Best Practices, SEO)
23. สรุปบท
✅ Animation: 90% ใช้ CSS transition ก็พอ — ใช้ Motion เมื่อต้อง state-based
✅ Motion (Framer Motion) = standard animation library ปี 2026
✅ AnimatePresence สำหรับ unmount animation, layout prop สำหรับ shared layout
✅ Performance: profile ก่อน optimize เสมอ
✅ React 19 Compiler → memoization auto — ไม่ต้องใส่ useMemo/useCallback มือ
✅ Virtual list (@tanstack/react-virtual) สำหรับ list ใหญ่
✅ Code split + lazy load → ลด initial bundle
✅ Track Web Vitals (LCP, INP, CLS) — เป้าหมายจริงคือ user experience 👀 View Transitions API (document.startViewTransition) — (ไม่ได้ cover ในบทนี้ — อ่านเพิ่มเติมได้เอง) API ใหม่ของ browser สำหรับ animate ระหว่างเปลี่ยน state/route แบบ native (ไม่ต้องใช้ library) Chrome/Edge support, polyfill ได้ (polyfill = โค้ดเสริมสำหรับ browser เก่าที่ยังไม่รองรับ) 👀 CSS Scroll-driven animation (animation-timeline: scroll) — (ไม่ได้ cover ในบทนี้) = animate ตาม scroll โดยไม่ใช้ JS