คู่มือฉบับสมบูรณ์เพื่อเพิ่มประสิทธิภาพแอปพลิเคชัน React ด้วย useMemo, useCallback และ React.memo เรียนรู้วิธีป้องกันการ re-render ที่ไม่จำเป็นและปรับปรุงประสบการณ์ผู้ใช้
การเพิ่มประสิทธิภาพ React: เชี่ยวชาญ useMemo, useCallback, และ React.memo
React ซึ่งเป็นไลบรารี JavaScript ยอดนิยมสำหรับสร้างส่วนติดต่อผู้ใช้ (user interface) เป็นที่รู้จักกันดีในเรื่องสถาปัตยกรรมแบบคอมโพเนนต์และสไตล์การเขียนโค้ดแบบประกาศ (declarative style) อย่างไรก็ตาม เมื่อแอปพลิเคชันมีความซับซ้อนมากขึ้น ประสิทธิภาพอาจกลายเป็นปัญหา การ re-render คอมโพเนนต์ที่ไม่จำเป็นอาจนำไปสู่ประสิทธิภาพที่เชื่องช้าและประสบการณ์ผู้ใช้ที่ไม่ดี โชคดีที่ React มีเครื่องมือหลายอย่างเพื่อเพิ่มประสิทธิภาพ รวมถึง useMemo
, useCallback
, และ React.memo
คู่มือนี้จะเจาะลึกเทคนิคเหล่านี้ พร้อมตัวอย่างที่นำไปใช้ได้จริงและข้อมูลเชิงลึกที่นำไปปฏิบัติได้ เพื่อช่วยให้คุณสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูง
ทำความเข้าใจการ Re-render ของ React
ก่อนที่จะลงลึกในเทคนิคการเพิ่มประสิทธิภาพ สิ่งสำคัญคือต้องเข้าใจว่าทำไมการ re-render ถึงเกิดขึ้นใน React เมื่อ state หรือ props ของคอมโพเนนต์เปลี่ยนแปลง React จะทริกเกอร์การ re-render ของคอมโพเนนต์นั้น และอาจรวมถึงคอมโพเนนต์ลูกของมันด้วย React ใช้ virtual DOM เพื่ออัปเดต DOM จริงอย่างมีประสิทธิภาพ แต่การ re-render ที่มากเกินไปก็ยังส่งผลกระทบต่อประสิทธิภาพได้ โดยเฉพาะในแอปพลิเคชันที่ซับซ้อน ลองจินตนาการถึงแพลตฟอร์มอีคอมเมิร์ซระดับโลกที่ราคาสินค้าอัปเดตบ่อยครั้ง หากไม่มีการปรับปรุงประสิทธิภาพ แม้แต่การเปลี่ยนแปลงราคาเพียงเล็กน้อยก็อาจทริกเกอร์การ re-render ทั่วทั้งรายการสินค้า ซึ่งส่งผลกระทบต่อการท่องเว็บของผู้ใช้
ทำไม Component ถึง Re-render
- การเปลี่ยนแปลง State: เมื่อ state ของคอมโพเนนต์ถูกอัปเดตโดยใช้
useState
หรือuseReducer
React จะ re-render คอมโพเนนต์นั้น - การเปลี่ยนแปลง Prop: หากคอมโพเนนต์ได้รับ props ใหม่จากคอมโพเนนต์แม่ คอมโพเนนต์นั้นจะ re-render
- การ Re-render ของ Parent: เมื่อคอมโพเนนต์แม่ re-render คอมโพเนนต์ลูกของมันก็จะ re-render ตามไปด้วยโดยปริยาย ไม่ว่า props ของมันจะเปลี่ยนแปลงหรือไม่ก็ตาม
- การเปลี่ยนแปลง Context: คอมโพเนนต์ที่ใช้ React Context จะ re-render เมื่อค่าของ context เปลี่ยนแปลง
เป้าหมายของการเพิ่มประสิทธิภาพคือการป้องกันการ re-render ที่ไม่จำเป็น เพื่อให้แน่ใจว่าคอมโพเนนต์จะอัปเดตก็ต่อเมื่อข้อมูลของมันเปลี่ยนแปลงจริงๆ เท่านั้น ลองพิจารณาสถานการณ์ที่เกี่ยวข้องกับการแสดงข้อมูลแบบเรียลไทม์สำหรับการวิเคราะห์ตลาดหุ้น หากคอมโพเนนต์แผนภูมิ re-render โดยไม่จำเป็นทุกครั้งที่มีการอัปเดตข้อมูลเล็กน้อย แอปพลิเคชันจะกลายเป็นไม่ตอบสนอง การเพิ่มประสิทธิภาพการ re-render จะช่วยให้มั่นใจได้ถึงประสบการณ์ผู้ใช้ที่ราบรื่นและตอบสนองได้ดี
แนะนำ useMemo: การจำค่าการคำนวณที่สิ้นเปลือง
useMemo
เป็น React hook ที่ทำการจำ (memoize) ผลลัพธ์ของการคำนวณ การจำค่า (Memoization) เป็นเทคนิคการเพิ่มประสิทธิภาพที่เก็บผลลัพธ์ของการเรียกใช้ฟังก์ชันที่สิ้นเปลือง และนำผลลัพธ์เหล่านั้นกลับมาใช้ใหม่เมื่อมีการป้อนข้อมูลเดิมอีกครั้ง ซึ่งช่วยป้องกันความจำเป็นในการรันฟังก์ชันซ้ำโดยไม่จำเป็น
ควรใช้ useMemo เมื่อใด
- การคำนวณที่สิ้นเปลือง: เมื่อคอมโพเนนต์จำเป็นต้องทำการคำนวณที่ใช้พลังประมวลผลสูงโดยอิงจาก props หรือ state ของมัน
- Referential Equality: เมื่อส่งค่าเป็น prop ไปยังคอมโพเนนต์ลูกที่อาศัย referential equality (การเปรียบเทียบตำแหน่งในหน่วยความจำ) เพื่อตัดสินใจว่าจะ re-render หรือไม่
useMemo ทำงานอย่างไร
useMemo
รับอาร์กิวเมนต์สองตัว:
- ฟังก์ชันที่ทำการคำนวณ
- อาร์เรย์ของ dependencies (ค่าที่ฟังก์ชันต้องพึ่งพา)
ฟังก์ชันจะถูกเรียกใช้ก็ต่อเมื่อหนึ่งใน dependencies ในอาร์เรย์มีการเปลี่ยนแปลงเท่านั้น มิฉะนั้น useMemo
จะส่งคืนค่าที่ถูกจำไว้ก่อนหน้านี้
ตัวอย่าง: การคำนวณลำดับฟีโบนัชชี
ลำดับฟีโบนัชชีเป็นตัวอย่างคลาสสิกของการคำนวณที่ใช้พลังประมวลผลสูง มาสร้างคอมโพเนนต์ที่คำนวณเลขฟีโบนัชชีตัวที่ n โดยใช้ useMemo
กัน
import React, { useState, useMemo } from 'react';
function Fibonacci({ n }) {
const fibonacciNumber = useMemo(() => {
console.log('Calculating Fibonacci...'); // แสดงให้เห็นว่าการคำนวณทำงานเมื่อใด
function calculateFibonacci(num) {
if (num <= 1) {
return num;
}
return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
}
return calculateFibonacci(n);
}, [n]);
return Fibonacci({n}) = {fibonacciNumber}
;
}
function App() {
const [number, setNumber] = useState(5);
return (
setNumber(parseInt(e.target.value))}
/>
);
}
export default App;
ในตัวอย่างนี้ ฟังก์ชัน calculateFibonacci
จะถูกเรียกใช้ก็ต่อเมื่อ prop n
เปลี่ยนแปลงเท่านั้น หากไม่มี useMemo
ฟังก์ชันนี้จะถูกเรียกใช้ทุกครั้งที่มีการ re-render ของคอมโพเนนต์ Fibonacci
แม้ว่า n
จะยังคงเดิมก็ตาม ลองจินตนาการว่าการคำนวณนี้เกิดขึ้นบนแดชบอร์ดทางการเงินระดับโลก ทุกครั้งที่ตลาดขยับจะทำให้เกิดการคำนวณใหม่ทั้งหมด ซึ่งนำไปสู่ความล่าช้าอย่างมาก useMemo
ช่วยป้องกันปัญหานั้นได้
แนะนำ useCallback: การจำฟังก์ชัน
useCallback
เป็นอีกหนึ่ง React hook ที่ทำการจำฟังก์ชัน ซึ่งจะป้องกันการสร้างอินสแตนซ์ฟังก์ชันใหม่ในทุกๆ การ render ซึ่งมีประโยชน์อย่างยิ่งเมื่อส่งฟังก์ชัน callback เป็น props ไปยังคอมโพเนนต์ลูก
ควรใช้ useCallback เมื่อใด
- การส่ง Callbacks เป็น Props: เมื่อส่งฟังก์ชันเป็น prop ไปยังคอมโพเนนต์ลูกที่ใช้
React.memo
หรือshouldComponentUpdate
เพื่อเพิ่มประสิทธิภาพการ re-render - Event Handlers: เมื่อกำหนดฟังก์ชันจัดการอีเวนต์ภายในคอมโพเนนต์เพื่อป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ลูก
useCallback ทำงานอย่างไร
useCallback
รับอาร์กิวเมนต์สองตัว:
- ฟังก์ชันที่ต้องการจำ
- อาร์เรย์ของ dependencies
ฟังก์ชันจะถูกสร้างขึ้นใหม่ก็ต่อเมื่อหนึ่งใน dependencies ในอาร์เรย์มีการเปลี่ยนแปลงเท่านั้น มิฉะนั้น useCallback
จะส่งคืนอินสแตนซ์ของฟังก์ชันเดิม
ตัวอย่าง: การจัดการการคลิกปุ่ม
มาสร้างคอมโพเนนต์ที่มีปุ่มที่ทริกเกอร์ฟังก์ชัน callback กัน เราจะใช้ useCallback
เพื่อจำฟังก์ชัน callback นั้น
import React, { useState, useCallback } from 'react';
function Button({ onClick, children }) {
console.log('Button re-rendered'); // แสดงให้เห็นว่า Button re-render เมื่อใด
return ;
}
const MemoizedButton = React.memo(Button);
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
setCount((prevCount) => prevCount + 1);
}, []); // dependency array ที่ว่างเปล่าหมายความว่าฟังก์ชันถูกสร้างขึ้นเพียงครั้งเดียว
return (
Count: {count}
Increment
);
}
export default App;
ในตัวอย่างนี้ ฟังก์ชัน handleClick
ถูกสร้างขึ้นเพียงครั้งเดียวเนื่องจาก dependency array ว่างเปล่า เมื่อคอมโพเนนต์ App
re-render เนื่องจากการเปลี่ยนแปลงของ state count
ฟังก์ชัน handleClick
จะยังคงเป็นตัวเดิม คอมโพเนนต์ MemoizedButton
ที่ถูกห่อด้วย React.memo
จะ re-render ก็ต่อเมื่อ props ของมันเปลี่ยนแปลงเท่านั้น เนื่องจาก prop onClick
(คือ handleClick
) ยังคงเหมือนเดิม คอมโพเนนต์ Button
จึงไม่ re-render โดยไม่จำเป็น ลองนึกถึงแอปพลิเคชันแผนที่แบบอินเทอร์แอคทีฟ ทุกครั้งที่ผู้ใช้โต้ตอบ อาจมีคอมโพเนนต์ปุ่มหลายสิบตัวที่ได้รับผลกระทบ หากไม่มี useCallback
ปุ่มเหล่านี้จะ re-render โดยไม่จำเป็น ทำให้ประสบการณ์การใช้งานกระตุก การใช้ useCallback
จะช่วยให้การโต้ตอบราบรื่นขึ้น
แนะนำ React.memo: การจำ Component
React.memo
เป็น higher-order component (HOC) ที่ทำการจำ functional component มันจะป้องกันไม่ให้คอมโพเนนต์ re-render หาก props ของมันไม่มีการเปลี่ยนแปลง ซึ่งคล้ายกับ PureComponent
สำหรับ class components
ควรใช้ React.memo เมื่อใด
- Pure Components: เมื่อผลลัพธ์ของคอมโพเนนต์ขึ้นอยู่กับ props ของมันเพียงอย่างเดียว และไม่มี state เป็นของตัวเอง
- การ Render ที่สิ้นเปลือง: เมื่อกระบวนการ render ของคอมโพเนนต์ใช้พลังประมวลผลสูง
- การ Re-render บ่อยครั้ง: เมื่อคอมโพเนนต์ถูก re-render บ่อยครั้งแม้ว่า props ของมันจะไม่ได้เปลี่ยนแปลงก็ตาม
React.memo ทำงานอย่างไร
React.memo
จะห่อหุ้ม functional component และทำการเปรียบเทียบ props ก่อนหน้าและ props ปัจจุบันแบบตื้นๆ (shallow comparison) หาก props เหมือนกัน คอมโพเนนต์จะไม่ re-render
ตัวอย่าง: การแสดงโปรไฟล์ผู้ใช้
มาสร้างคอมโพเนนต์ที่แสดงโปรไฟล์ผู้ใช้กัน เราจะใช้ React.memo
เพื่อป้องกันการ re-render ที่ไม่จำเป็นหากข้อมูลของผู้ใช้ไม่มีการเปลี่ยนแปลง
import React from 'react';
function UserProfile({ user }) {
console.log('UserProfile re-rendered'); // แสดงให้เห็นว่า component re-render เมื่อใด
return (
Name: {user.name}
Email: {user.email}
);
}
const MemoizedUserProfile = React.memo(UserProfile, (prevProps, nextProps) => {
// ฟังก์ชันเปรียบเทียบแบบกำหนดเอง (optional)
return prevProps.user.id === nextProps.user.id; // re-render ต่อเมื่อ ID ของผู้ใช้เปลี่ยนแปลง
});
function App() {
const [user, setUser] = React.useState({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
});
const updateUser = () => {
setUser({ ...user, name: 'Jane Doe' }); // เปลี่ยนชื่อ
};
return (
);
}
export default App;
ในตัวอย่างนี้ คอมโพเนนต์ MemoizedUserProfile
จะ re-render ก็ต่อเมื่อ prop user.id
เปลี่ยนแปลงเท่านั้น แม้ว่าคุณสมบัติอื่นๆ ของอ็อบเจกต์ user
จะเปลี่ยนแปลง (เช่น ชื่อหรืออีเมล) คอมโพเนนต์ก็จะไม่ re-render เว้นแต่ ID จะแตกต่างกัน ฟังก์ชันเปรียบเทียบที่กำหนดเองนี้ภายใน `React.memo` ช่วยให้สามารถควบคุมการ re-render ของคอมโพเนนต์ได้อย่างละเอียด ลองนึกถึงแพลตฟอร์มโซเชียลมีเดียที่มีการอัปเดตโปรไฟล์ผู้ใช้อยู่ตลอดเวลา หากไม่มี `React.memo` การเปลี่ยนสถานะหรือรูปโปรไฟล์ของผู้ใช้อาจทำให้คอมโพเนนต์โปรไฟล์ทั้งหมด re-render แม้ว่ารายละเอียดหลักของผู้ใช้จะยังคงเหมือนเดิม `React.memo` ช่วยให้สามารถอัปเดตแบบเฉพาะเจาะจงและปรับปรุงประสิทธิภาพได้อย่างมาก
การใช้ useMemo, useCallback, และ React.memo ร่วมกัน
เทคนิคทั้งสามนี้จะมีประสิทธิภาพสูงสุดเมื่อใช้ร่วมกัน useMemo
ใช้จำการคำนวณที่สิ้นเปลือง useCallback
ใช้จำฟังก์ชัน และ React.memo
ใช้จำคอมโพเนนต์ การผสมผสานเทคนิคเหล่านี้จะช่วยให้คุณลดจำนวนการ re-render ที่ไม่จำเป็นในแอปพลิเคชัน React ของคุณได้อย่างมาก
ตัวอย่าง: Component ที่ซับซ้อน
มาสร้างคอมโพเนนต์ที่ซับซ้อนขึ้นเพื่อสาธิตวิธีการรวมเทคนิคเหล่านี้เข้าด้วยกัน
import React, { useState, useCallback, useMemo } from 'react';
function ListItem({ item, onUpdate, onDelete }) {
console.log(`ListItem ${item.id} re-rendered`); // แสดงให้เห็นว่า component re-render เมื่อใด
return (
{item.text}
);
}
const MemoizedListItem = React.memo(ListItem);
function List({ items, onUpdate, onDelete }) {
console.log('List re-rendered'); // แสดงให้เห็นว่า component re-render เมื่อใด
return (
{items.map((item) => (
))}
);
}
const MemoizedList = React.memo(List);
function App() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const handleUpdate = useCallback((id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, text: `Updated ${item.text}` } : item
)
);
}, []);
const handleDelete = useCallback((id) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
}, []);
const memoizedItems = useMemo(() => items, [items]);
return (
);
}
export default App;
ในตัวอย่างนี้:
useCallback
ถูกใช้เพื่อจำฟังก์ชันhandleUpdate
และhandleDelete
เพื่อป้องกันไม่ให้ถูกสร้างขึ้นใหม่ทุกครั้งที่ renderuseMemo
ถูกใช้เพื่อจำอาร์เรย์items
เพื่อป้องกันไม่ให้คอมโพเนนต์List
re-render หาก reference ของอาร์เรย์ไม่ได้เปลี่ยนแปลงReact.memo
ถูกใช้เพื่อจำคอมโพเนนต์ListItem
และList
เพื่อป้องกันไม่ให้ re-render หาก props ของมันไม่ได้เปลี่ยนแปลง
การผสมผสานเทคนิคเหล่านี้ช่วยให้แน่ใจว่าคอมโพเนนต์จะ re-render เฉพาะเมื่อจำเป็นเท่านั้น ซึ่งนำไปสู่การปรับปรุงประสิทธิภาพอย่างมาก ลองจินตนาการถึงเครื่องมือจัดการโครงการขนาดใหญ่ที่มีรายการงานที่ถูกอัปเดต ลบ และจัดลำดับใหม่อยู่ตลอดเวลา หากไม่มีการปรับปรุงประสิทธิภาพเหล่านี้ การเปลี่ยนแปลงเล็กน้อยในรายการงานจะทริกเกอร์การ re-render เป็นทอดๆ ทำให้แอปพลิเคชันช้าและไม่ตอบสนอง การใช้ useMemo
, useCallback
, และ React.memo
อย่างมีกลยุทธ์จะช่วยให้แอปพลิเคชันยังคงมีประสิทธิภาพแม้จะมีข้อมูลที่ซับซ้อนและการอัปเดตบ่อยครั้ง
เทคนิคการเพิ่มประสิทธิภาพเพิ่มเติม
แม้ว่า useMemo
, useCallback
, และ React.memo
จะเป็นเครื่องมือที่ทรงพลัง แต่ก็ไม่ใช่ทางเลือกเดียวในการเพิ่มประสิทธิภาพ React นี่คือเทคนิคเพิ่มเติมบางส่วนที่ควรพิจารณา:
- Code Splitting: แบ่งแอปพลิเคชันของคุณออกเป็นส่วนเล็กๆ (chunks) ที่สามารถโหลดได้ตามความต้องการ ซึ่งจะช่วยลดเวลาในการโหลดเริ่มต้นและปรับปรุงประสิทธิภาพโดยรวม
- Lazy Loading: โหลดคอมโพเนนต์และทรัพยากรเฉพาะเมื่อจำเป็นเท่านั้น ซึ่งมีประโยชน์อย่างยิ่งสำหรับรูปภาพและเนื้อหาขนาดใหญ่อื่นๆ
- Virtualization: Render เฉพาะส่วนที่มองเห็นได้ของรายการหรือตารางขนาดใหญ่ ซึ่งสามารถปรับปรุงประสิทธิภาพได้อย่างมากเมื่อต้องจัดการกับชุดข้อมูลขนาดใหญ่ ไลบรารีอย่าง
react-window
และreact-virtualized
สามารถช่วยในเรื่องนี้ได้ - Debouncing และ Throttling: จำกัดอัตราการเรียกใช้ฟังก์ชัน ซึ่งมีประโยชน์สำหรับการจัดการอีเวนต์ต่างๆ เช่น การเลื่อน (scrolling) และการปรับขนาด (resizing)
- Immutability: ใช้โครงสร้างข้อมูลที่ไม่สามารถเปลี่ยนแปลงได้ (immutable data structures) เพื่อหลีกเลี่ยงการเปลี่ยนแปลงข้อมูลโดยไม่ได้ตั้งใจและทำให้การตรวจจับการเปลี่ยนแปลงง่ายขึ้น
ข้อควรพิจารณาในการเพิ่มประสิทธิภาพสำหรับผู้ใช้ทั่วโลก
เมื่อเพิ่มประสิทธิภาพแอปพลิเคชัน React สำหรับผู้ใช้ทั่วโลก สิ่งสำคัญคือต้องพิจารณาปัจจัยต่างๆ เช่น ความหน่วงของเครือข่าย ความสามารถของอุปกรณ์ และการแปลภาษา นี่คือเคล็ดลับบางประการ:
- Content Delivery Networks (CDNs): ใช้ CDN เพื่อให้บริการไฟล์ static จากตำแหน่งที่อยู่ใกล้กับผู้ใช้ของคุณมากขึ้น ซึ่งจะช่วยลดความหน่วงของเครือข่ายและปรับปรุงเวลาในการโหลด
- Image Optimization: ปรับขนาดรูปภาพให้เหมาะสมกับขนาดหน้าจอและความละเอียดต่างๆ ใช้เทคนิคการบีบอัดเพื่อลดขนาดไฟล์
- Localization: โหลดเฉพาะทรัพยากรภาษาที่จำเป็นสำหรับผู้ใช้แต่ละคน ซึ่งจะช่วยลดเวลาในการโหลดเริ่มต้นและปรับปรุงประสบการณ์ผู้ใช้
- Adaptive Loading: ตรวจจับการเชื่อมต่อเครือข่ายและความสามารถของอุปกรณ์ของผู้ใช้ และปรับพฤติกรรมของแอปพลิเคชันให้สอดคล้องกัน ตัวอย่างเช่น คุณอาจปิดใช้งานแอนิเมชันหรือลดคุณภาพของภาพสำหรับผู้ใช้ที่มีการเชื่อมต่อเครือข่ายช้าหรือใช้อุปกรณ์รุ่นเก่า
สรุป
การเพิ่มประสิทธิภาพแอปพลิเคชัน React เป็นสิ่งสำคัญอย่างยิ่งในการมอบประสบการณ์ผู้ใช้ที่ราบรื่นและตอบสนองได้ดี ด้วยการเรียนรู้เทคนิคต่างๆ เช่น useMemo
, useCallback
, และ React.memo
และการพิจารณากลยุทธ์การเพิ่มประสิทธิภาพระดับโลก คุณสามารถสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูงซึ่งสามารถปรับขนาดเพื่อตอบสนองความต้องการของผู้ใช้ที่หลากหลายได้ อย่าลืมทำการโปรไฟล์แอปพลิเคชันของคุณเพื่อระบุจุดคอขวดของประสิทธิภาพและใช้เทคนิคการเพิ่มประสิทธิภาพเหล่านี้อย่างมีกลยุทธ์ อย่าเพิ่มประสิทธิภาพก่อนเวลาอันควร – มุ่งเน้นไปที่ส่วนที่คุณจะได้รับผลกระทบที่สำคัญที่สุด
คู่มือนี้เป็นรากฐานที่มั่นคงสำหรับความเข้าใจและการนำการเพิ่มประสิทธิภาพ React ไปใช้ ในขณะที่คุณพัฒนาแอปพลิเคชัน React ต่อไป อย่าลืมให้ความสำคัญกับประสิทธิภาพและแสวงหาวิธีใหม่ๆ ในการปรับปรุงประสบการณ์ผู้ใช้อยู่เสมอ