Skip to content

บทที่ 10 — Animation + Performance

← บทที่ 9 | สารบัญ | บทที่ 11 →

🔴 ระดับ: สูง — อ่านทีหลังได้ เรื่อง 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-groupclassicAPI เก่า
Framer Motion / Motiondeclarative¹, smooth (ลื่น), gesture²bundle ขนาดกลาง
react-springphysics-based, smoothlearning curve (ใช้เวลาทำความเข้าใจนานกว่า)
GSAPpowerful (timeline)imperative (เชิงสั่งการ — บอกทีละขั้นว่าให้ทำอะไร)
Auto Animateone-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, delay
  • whileHover, 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 transition

layout 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/react
tsx
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." — อย่าเพิ่งรีบจูนให้เร็วถ้ายังไม่รู้ว่าตรงไหนช้าจริง)

ทำตามลำดับ:

  1. เขียน code ปกติ
  2. วัด ก่อน (Profiler, Web Vitals)
  3. หา bottleneck จริง
  4. แก้เฉพาะที่ช้าจริง

อย่าเริ่มด้วยการใส่ useMemo ทุกที่ — มันมี cost ของตัวมัน


12. React Profiler

ใน Chrome DevTools → Profiler tab (ติด React DevTools extension)

  1. กด record
  2. ทำ interaction
  3. กด stop
  4. ดู:
    • 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-compiler
ts
// 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 ทำงาน

  1. Component + hook ต้อง pure (บริสุทธิ์ — output เดิมทุกครั้งที่รับ input เดิม) — ไม่มี side effect (ผลข้างเคียง เช่น เรียก API, แก้ DOM, อ่าน/เขียนไฟล์) ใน body
  2. ห้าม mutate props/state/ref ตรง ๆ
  3. เรียก hook ตามลำดับเดียวกันทุก render
  4. ไม่ใช้ 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 พร้อม custom arePropsEqual)

อย่าเริ่มจาก "ใส่ 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-virtual
tsx
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-visualizer
ts
// 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-vitals
tsx
// ต้องการ 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 เดียวกัน หรืออยากได้ isPending indicator ให้ใช้ 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

  • หลีกเลี่ยง scroll event ที่ทำงานหนัก — ใช้ 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 ก่อน measureprofile ก่อนเสมอ
Bundle ใหญ่ตอน first loadcode split + lazy load
Render list 10k row ตรง ๆvirtual list
Inline <img> ใหญ่ ไม่มี lazy + responsiveloading="lazy" + srcset
Layout shift จาก image / fontreserve 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

  1. รัน npm run build ดู bundle ขนาด
  2. ใช้ visualizer หา dependency ใหญ่
  3. ลด: lazy load route, dynamic import chart lib, remove unused deps
  4. เปรียบเทียบขนาด 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


← บทที่ 9 | บทที่ 11 → Next.js