คู่มือฉบับสมบูรณ์เกี่ยวกับ React useCallback สำรวจเทคนิค function memoization เพื่อเพิ่มประสิทธิภาพในแอปพลิเคชัน React เรียนรู้วิธีป้องกันการ re-render ที่ไม่จำเป็นและเพิ่มประสิทธิภาพ
React useCallback: การเรียนรู้ Memoization ของฟังก์ชันเพื่อการเพิ่มประสิทธิภาพสูงสุด
ในโลกของการพัฒนา React การเพิ่มประสิทธิภาพเป็นสิ่งสำคัญยิ่งในการมอบประสบการณ์ผู้ใช้ที่ราบรื่นและตอบสนองได้ดี หนึ่งในเครื่องมือที่ทรงพลังในคลังแสงของนักพัฒนา React เพื่อให้บรรลุเป้าหมายนี้คือ useCallback
ซึ่งเป็น React Hook ที่ช่วยให้สามารถทำ memoization ของฟังก์ชันได้ คู่มือฉบับสมบูรณ์นี้จะเจาะลึกรายละเอียดของ useCallback
สำรวจวัตถุประสงค์ ประโยชน์ และการใช้งานจริงในการเพิ่มประสิทธิภาพคอมโพเนนต์ของ React
ทำความเข้าใจเกี่ยวกับ Function Memoization
โดยแก่นแท้แล้ว memoization คือเทคนิคการเพิ่มประสิทธิภาพที่เกี่ยวข้องกับการแคชผลลัพธ์ของการเรียกใช้ฟังก์ชันที่มีค่าใช้จ่ายสูง และส่งคืนผลลัพธ์ที่แคชไว้เมื่อมีการเรียกใช้อีกครั้งด้วยอินพุตเดียวกัน ในบริบทของ React, function memoization ด้วย useCallback
จะมุ่งเน้นไปที่การรักษาตัวตน (identity) ของฟังก์ชันในระหว่างการเรนเดอร์แต่ละครั้ง เพื่อป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ลูกที่ต้องพึ่งพาฟังก์ชันนั้น
หากไม่มี useCallback
อินสแตนซ์ของฟังก์ชันใหม่จะถูกสร้างขึ้นทุกครั้งที่ functional component ทำการเรนเดอร์ แม้ว่าตรรกะและ dependencies ของฟังก์ชันจะยังคงไม่เปลี่ยนแปลง สิ่งนี้อาจนำไปสู่ปัญหาคอขวดด้านประสิทธิภาพเมื่อฟังก์ชันเหล่านี้ถูกส่งเป็น props ไปยังคอมโพเนนต์ลูก ทำให้คอมโพเนนต์เหล่านั้นต้อง re-render โดยไม่จำเป็น
ขอแนะนำ useCallback
Hook
useCallback
Hook เป็นวิธีการทำ memoize ฟังก์ชันใน functional component ของ React โดยจะรับอาร์กิวเมนต์สองตัว:
- ฟังก์ชันที่จะทำ memoize
- อาร์เรย์ของ dependencies
useCallback
จะคืนค่าฟังก์ชันเวอร์ชันที่ทำ memoize แล้ว ซึ่งจะเปลี่ยนแปลงก็ต่อเมื่อ dependencies ตัวใดตัวหนึ่งในอาร์เรย์ของ dependencies มีการเปลี่ยนแปลงระหว่างการเรนเดอร์
นี่คือตัวอย่างพื้นฐาน:
import React, { useCallback } from 'react';
function MyComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked!');
}, []); // อาร์เรย์ของ dependency ที่ว่างเปล่า
return ;
}
export default MyComponent;
ในตัวอย่างนี้ ฟังก์ชัน handleClick
ถูกทำ memoize โดยใช้ useCallback
พร้อมกับอาร์เรย์ของ dependency ที่ว่างเปล่า ([]
) ซึ่งหมายความว่าฟังก์ชัน handleClick
จะถูกสร้างขึ้นเพียงครั้งเดียวเมื่อคอมโพเนนต์เรนเดอร์ครั้งแรก และตัวตนของมันจะยังคงเหมือนเดิมในการ re-render ครั้งต่อๆ ไป prop onClick
ของปุ่มจะได้รับอินสแตนซ์ของฟังก์ชันเดียวกันเสมอ ซึ่งช่วยป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ปุ่ม (หากเป็นคอมโพเนนต์ที่ซับซ้อนกว่าซึ่งจะได้รับประโยชน์จาก memoization)
ประโยชน์ของการใช้ useCallback
- ป้องกันการ Re-renders ที่ไม่จำเป็น: ประโยชน์หลักของ
useCallback
คือการป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ลูก เมื่อฟังก์ชันที่ส่งเป็น prop เปลี่ยนแปลงทุกครั้งที่เรนเดอร์ จะทำให้คอมโพเนนต์ลูก re-render แม้ว่าข้อมูลพื้นฐานจะไม่ได้เปลี่ยนแปลงก็ตาม การทำ memoize ฟังก์ชันด้วยuseCallback
ช่วยให้มั่นใจได้ว่าอินสแตนซ์ของฟังก์ชันเดียวกันจะถูกส่งลงไป หลีกเลี่ยงการ re-render ที่ไม่จำเป็น - การเพิ่มประสิทธิภาพ: ด้วยการลดจำนวนการ re-render,
useCallback
มีส่วนช่วยในการปรับปรุงประสิทธิภาพอย่างมีนัยสำคัญ โดยเฉพาะในแอปพลิเคชันที่ซับซ้อนซึ่งมีคอมโพเนนต์ซ้อนกันลึกๆ - ปรับปรุงความสามารถในการอ่านโค้ด: การใช้
useCallback
สามารถทำให้โค้ดของคุณอ่านง่ายและบำรุงรักษาได้ดีขึ้น โดยการประกาศ dependencies ของฟังก์ชันอย่างชัดเจน ซึ่งช่วยให้นักพัฒนาคนอื่นเข้าใจพฤติกรรมของฟังก์ชันและผลข้างเคียงที่อาจเกิดขึ้นได้
ตัวอย่างการใช้งานจริงและกรณีศึกษา
ตัวอย่างที่ 1: การเพิ่มประสิทธิภาพ List Component
พิจารณาสถานการณ์ที่คุณมีคอมโพเนนต์แม่ที่เรนเดอร์รายการของไอเท็มโดยใช้คอมโพเนนต์ลูกที่เรียกว่า ListItem
คอมโพเนนต์ ListItem
ได้รับ prop onItemClick
ซึ่งเป็นฟังก์ชันที่จัดการกับ click event สำหรับแต่ละไอเท็ม
import React, { useState, useCallback } from 'react';
function ListItem({ item, onItemClick }) {
console.log(`ListItem rendered for item: ${item.id}`);
return onItemClick(item.id)}>{item.name} ;
}
const MemoizedListItem = React.memo(ListItem);
function MyListComponent() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
const [selectedItemId, setSelectedItemId] = useState(null);
const handleItemClick = useCallback((id) => {
console.log(`Item clicked: ${id}`);
setSelectedItemId(id);
}, []); // ไม่มี dependencies ดังนั้นจึงไม่เคยเปลี่ยนแปลง
return (
{items.map(item => (
))}
);
}
export default MyListComponent;
ในตัวอย่างนี้ handleItemClick
ถูกทำ memoize โดยใช้ useCallback
สิ่งสำคัญคือคอมโพเนนต์ ListItem
ถูกห่อหุ้มด้วย React.memo
ซึ่งทำการเปรียบเทียบ props แบบตื้น (shallow comparison) เนื่องจาก handleItemClick
จะเปลี่ยนแปลงก็ต่อเมื่อ dependencies ของมันเปลี่ยนแปลง (ซึ่งในที่นี้ไม่มี เพราะอาร์เรย์ของ dependency ว่างเปล่า), React.memo
จะป้องกันไม่ให้ ListItem
re-render หาก state ของ `items` เปลี่ยนแปลง (เช่น ถ้าเราเพิ่มหรือลบไอเท็ม)
หากไม่มี useCallback
ฟังก์ชัน handleItemClick
ใหม่จะถูกสร้างขึ้นทุกครั้งที่ MyListComponent
เรนเดอร์ ทำให้ ListItem
แต่ละตัว re-render แม้ว่าข้อมูลของไอเท็มเองจะไม่ได้เปลี่ยนแปลงก็ตาม
ตัวอย่างที่ 2: การเพิ่มประสิทธิภาพ Form Component
พิจารณาฟอร์มคอมโพเนนต์ที่คุณมีช่องป้อนข้อมูลหลายช่องและปุ่มส่งข้อมูล ช่องป้อนข้อมูลแต่ละช่องมี onChange
handler ที่อัปเดต state ของคอมโพเนนต์ คุณสามารถใช้ useCallback
เพื่อทำ memoize onChange
handler เหล่านี้ ป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ลูกที่ต้องพึ่งพามัน
import React, { useState, useCallback } from 'react';
function MyFormComponent() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleNameChange = useCallback((event) => {
setName(event.target.value);
}, []);
const handleEmailChange = useCallback((event) => {
setEmail(event.target.value);
}, []);
const handleSubmit = useCallback((event) => {
event.preventDefault();
console.log(`Name: ${name}, Email: ${email}`);
}, [name, email]);
return (
);
}
export default MyFormComponent;
ในตัวอย่างนี้ handleNameChange
, handleEmailChange
, และ handleSubmit
ทั้งหมดถูกทำ memoize โดยใช้ useCallback
ฟังก์ชัน handleNameChange
และ handleEmailChange
มีอาร์เรย์ของ dependency ที่ว่างเปล่าเพราะพวกมันแค่ต้องการตั้งค่า state และไม่ได้พึ่งพาตัวแปรภายนอกใดๆ ส่วน handleSubmit
ขึ้นอยู่กับ state ของ `name` และ `email` ดังนั้นมันจะถูกสร้างขึ้นใหม่ก็ต่อเมื่อค่าใดค่าหนึ่งเปลี่ยนแปลง
ตัวอย่างที่ 3: การเพิ่มประสิทธิภาพแถบค้นหาแบบ Global
ลองจินตนาการว่าคุณกำลังสร้างเว็บไซต์สำหรับแพลตฟอร์มอีคอมเมิร์ซระดับโลกที่ต้องจัดการการค้นหาในภาษาและชุดอักขระต่างๆ แถบค้นหาเป็นคอมโพเนนต์ที่ซับซ้อน และคุณต้องการให้แน่ใจว่าประสิทธิภาพของมันได้รับการปรับให้เหมาะสมที่สุด
import React, { useState, useCallback } from 'react';
function SearchBar({ onSearch }) {
const [searchTerm, setSearchTerm] = useState('');
const handleInputChange = (event) => {
setSearchTerm(event.target.value);
};
const handleSearch = useCallback(() => {
onSearch(searchTerm);
}, [searchTerm, onSearch]);
return (
);
}
export default SearchBar;
ในตัวอย่างนี้ ฟังก์ชัน handleSearch
ถูกทำ memoize โดยใช้ useCallback
มันขึ้นอยู่กับ searchTerm
และ prop onSearch
(ซึ่งเราสมมติว่าถูกทำ memoize ในคอมโพเนนต์แม่ด้วย) สิ่งนี้ช่วยให้แน่ใจว่าฟังก์ชันการค้นหาจะถูกสร้างขึ้นใหม่ก็ต่อเมื่อคำค้นหาเปลี่ยนแปลงเท่านั้น ซึ่งป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์แถบค้นหาและคอมโพเนนต์ลูกใดๆ ที่อาจมี นี่เป็นสิ่งสำคัญอย่างยิ่งหาก `onSearch` ไปกระตุ้นการทำงานที่ใช้ทรัพยากรในการคำนวณสูง เช่น การกรองแคตตาล็อกสินค้าขนาดใหญ่
เมื่อใดที่ควรใช้ useCallback
แม้ว่า useCallback
จะเป็นเครื่องมือเพิ่มประสิทธิภาพที่ทรงพลัง แต่ก็ควรใช้อย่างรอบคอบ การใช้ useCallback
มากเกินไปอาจทำให้ประสิทธิภาพลดลงได้จริงเนื่องจากค่าใช้จ่ายในการสร้างและจัดการฟังก์ชันที่ทำ memoize
นี่คือแนวทางบางประการเกี่ยวกับเวลาที่ควรใช้ useCallback
:
- เมื่อส่งฟังก์ชันเป็น props ไปยังคอมโพเนนต์ลูกที่ถูกห่อหุ้มด้วย
React.memo
: นี่เป็นกรณีการใช้งานที่พบบ่อยและมีประสิทธิภาพที่สุดสำหรับuseCallback
โดยการทำ memoize ฟังก์ชัน คุณสามารถป้องกันไม่ให้คอมโพเนนต์ลูก re-render โดยไม่จำเป็น - เมื่อใช้ฟังก์ชันภายใน
useEffect
hooks: หากฟังก์ชันถูกใช้เป็น dependency ในuseEffect
hook การทำ memoize ด้วยuseCallback
สามารถป้องกันไม่ให้ effect ทำงานโดยไม่จำเป็นทุกครั้งที่เรนเดอร์ เนื่องจากตัวตนของฟังก์ชันจะเปลี่ยนแปลงก็ต่อเมื่อ dependencies ของมันเปลี่ยนแปลงเท่านั้น - เมื่อต้องจัดการกับฟังก์ชันที่ใช้ทรัพยากรในการคำนวณสูง: หากฟังก์ชันทำการคำนวณหรือดำเนินการที่ซับซ้อน การทำ memoize ด้วย
useCallback
สามารถประหยัดเวลาในการประมวลผลได้อย่างมากโดยการแคชผลลัพธ์
ในทางกลับกัน ควรหลีกเลี่ยงการใช้ useCallback
ในสถานการณ์ต่อไปนี้:
- สำหรับฟังก์ชันง่ายๆ ที่ไม่มี dependencies: ค่าใช้จ่ายในการทำ memoize ฟังก์ชันง่ายๆ อาจไม่คุ้มค่ากับประโยชน์ที่ได้รับ
- เมื่อ dependencies ของฟังก์ชันเปลี่ยนแปลงบ่อย: หาก dependencies ของฟังก์ชันเปลี่ยนแปลงอยู่ตลอดเวลา ฟังก์ชันที่ทำ memoize จะถูกสร้างขึ้นใหม่ทุกครั้งที่เรนเดอร์ ซึ่งจะลบล้างประโยชน์ด้านประสิทธิภาพ
- เมื่อคุณไม่แน่ใจว่าจะช่วยปรับปรุงประสิทธิภาพได้หรือไม่: ควรทำการวัดประสิทธิภาพ (benchmark) โค้ดของคุณก่อนและหลังการใช้
useCallback
เสมอ เพื่อให้แน่ใจว่ามันช่วยปรับปรุงประสิทธิภาพได้จริง
ข้อผิดพลาดและข้อควรระวังทั่วไป
- ลืมใส่ Dependencies: ข้อผิดพลาดที่พบบ่อยที่สุดในการใช้
useCallback
คือการลืมใส่ dependencies ทั้งหมดของฟังก์ชันลงในอาร์เรย์ของ dependency ซึ่งอาจนำไปสู่ปัญหา stale closures และพฤติกรรมที่ไม่คาดคิด ควรพิจารณาอย่างรอบคอบเสมอว่าฟังก์ชันขึ้นอยู่กับตัวแปรใดบ้าง และใส่พวกมันลงในอาร์เรย์ของ dependency - การเพิ่มประสิทธิภาพที่มากเกินไป (Over-optimization): ดังที่กล่าวไว้ก่อนหน้านี้ การใช้
useCallback
มากเกินไปอาจทำให้ประสิทธิภาพลดลง ควรใช้เมื่อจำเป็นจริงๆ และเมื่อคุณมีหลักฐานว่ามันช่วยปรับปรุงประสิทธิภาพ - อาร์เรย์ของ Dependency ที่ไม่ถูกต้อง: การตรวจสอบให้แน่ใจว่า dependencies ถูกต้องเป็นสิ่งสำคัญอย่างยิ่ง ตัวอย่างเช่น หากคุณใช้ตัวแปร state ภายในฟังก์ชัน คุณต้องใส่มันลงในอาร์เรย์ของ dependency เพื่อให้แน่ใจว่าฟังก์ชันจะได้รับการอัปเดตเมื่อ state เปลี่ยนแปลง
ทางเลือกอื่นนอกเหนือจาก useCallback
แม้ว่า useCallback
จะเป็นเครื่องมือที่ทรงพลัง แต่ก็มีแนวทางอื่นในการเพิ่มประสิทธิภาพของฟังก์ชันใน React:
React.memo
: ดังที่แสดงในตัวอย่าง การห่อหุ้มคอมโพเนนต์ลูกด้วยReact.memo
สามารถป้องกันไม่ให้พวกมัน re-render หาก props ไม่ได้เปลี่ยนแปลง ซึ่งมักใช้ร่วมกับuseCallback
เพื่อให้แน่ใจว่า function props ที่ส่งไปยังคอมโพเนนต์ลูกยังคงเสถียรuseMemo
:useMemo
hook คล้ายกับuseCallback
แต่จะทำ memoize *ผลลัพธ์* ของการเรียกใช้ฟังก์ชันแทนที่จะเป็นตัวฟังก์ชันเอง ซึ่งจะมีประโยชน์สำหรับการทำ memoize การคำนวณที่มีค่าใช้จ่ายสูงหรือการแปลงข้อมูล- Code Splitting: Code splitting เกี่ยวข้องกับการแบ่งแอปพลิเคชันของคุณออกเป็นส่วนเล็กๆ ที่จะถูกโหลดเมื่อต้องการ ซึ่งสามารถปรับปรุงเวลาในการโหลดเริ่มต้นและประสิทธิภาพโดยรวมได้
- Virtualization: เทคนิค Virtualization เช่น windowing สามารถปรับปรุงประสิทธิภาพเมื่อเรนเดอร์รายการข้อมูลขนาดใหญ่โดยการเรนเดอร์เฉพาะรายการที่มองเห็นได้เท่านั้น
useCallback
และ Referential Equality
useCallback
ช่วยให้มั่นใจในเรื่อง referential equality สำหรับฟังก์ชันที่ทำ memoize ซึ่งหมายความว่าตัวตนของฟังก์ชัน (เช่น การอ้างอิงถึงฟังก์ชันในหน่วยความจำ) จะยังคงเหมือนเดิมในระหว่างการเรนเดอร์ ตราบใดที่ dependencies ไม่ได้เปลี่ยนแปลง นี่เป็นสิ่งสำคัญสำหรับการเพิ่มประสิทธิภาพคอมโพเนนต์ที่อาศัยการตรวจสอบความเท่ากันแบบเข้มงวด (strict equality check) เพื่อตัดสินใจว่าจะ re-render หรือไม่ โดยการรักษาตัวตนของฟังก์ชันให้เหมือนเดิม useCallback
จะช่วยป้องกันการ re-render ที่ไม่จำเป็นและปรับปรุงประสิทธิภาพโดยรวม
ตัวอย่างในโลกแห่งความเป็นจริง: การปรับขนาดสู่แอปพลิเคชันระดับโลก
เมื่อพัฒนาแอปพลิเคชันสำหรับผู้ชมทั่วโลก ประสิทธิภาพจะยิ่งมีความสำคัญมากขึ้น เวลาในการโหลดที่ช้าหรือการโต้ตอบที่อืดอาดอาจส่งผลกระทบอย่างมากต่อประสบการณ์ของผู้ใช้ โดยเฉพาะในภูมิภาคที่มีการเชื่อมต่ออินเทอร์เน็ตที่ช้ากว่า
- Internationalization (i18n): ลองจินตนาการถึงฟังก์ชันที่จัดรูปแบบวันที่และตัวเลขตามสถานที่ (locale) ของผู้ใช้ การทำ memoize ฟังก์ชันนี้ด้วย
useCallback
สามารถป้องกันการ re-render ที่ไม่จำเป็นเมื่อ locale ไม่ได้เปลี่ยนแปลงบ่อย โดย locale จะเป็น dependency - ชุดข้อมูลขนาดใหญ่: เมื่อแสดงชุดข้อมูลขนาดใหญ่ในตารางหรือรายการ การทำ memoize ฟังก์ชันที่รับผิดชอบในการกรอง การเรียงลำดับ และการแบ่งหน้า (pagination) สามารถปรับปรุงประสิทธิภาพได้อย่างมาก
- การทำงานร่วมกันแบบเรียลไทม์: ในแอปพลิเคชันที่ทำงานร่วมกัน เช่น โปรแกรมแก้ไขเอกสารออนไลน์ การทำ memoize ฟังก์ชันที่จัดการการป้อนข้อมูลของผู้ใช้และการซิงโครไนซ์ข้อมูลสามารถลดความหน่วงและปรับปรุงการตอบสนองได้
แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ useCallback
- ใส่ dependencies ทั้งหมดเสมอ: ตรวจสอบซ้ำอีกครั้งว่าอาร์เรย์ของ dependency ของคุณมีตัวแปรทั้งหมดที่ใช้ภายในฟังก์ชัน
useCallback
- ใช้ร่วมกับ
React.memo
: จับคู่useCallback
กับReact.memo
เพื่อผลลัพธ์ด้านประสิทธิภาพที่ดีที่สุด - วัดประสิทธิภาพโค้ดของคุณ: วัดผลกระทบด้านประสิทธิภาพของ
useCallback
ก่อนและหลังการนำไปใช้ - ทำให้ฟังก์ชันมีขนาดเล็กและมุ่งเน้น: ฟังก์ชันที่เล็กและมีจุดประสงค์เฉพาะเจาะจงจะง่ายต่อการทำ memoize และเพิ่มประสิทธิภาพ
- พิจารณาใช้ linter: Linter สามารถช่วยคุณระบุ dependencies ที่ขาดหายไปในการเรียกใช้
useCallback
ของคุณได้
สรุป
useCallback
เป็นเครื่องมือที่มีค่าสำหรับการเพิ่มประสิทธิภาพในแอปพลิเคชัน React โดยการทำความเข้าใจวัตถุประสงค์ ประโยชน์ และการใช้งานจริง คุณสามารถป้องกันการ re-render ที่ไม่จำเป็นและปรับปรุงประสบการณ์ผู้ใช้โดยรวมได้อย่างมีประสิทธิภาพ อย่างไรก็ตาม สิ่งสำคัญคือต้องใช้ useCallback
อย่างรอบคอบและทำการวัดประสิทธิภาพโค้ดของคุณเพื่อให้แน่ใจว่ามันช่วยปรับปรุงประสิทธิภาพได้จริง ด้วยการปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดที่ระบุไว้ในคู่มือนี้ คุณสามารถเชี่ยวชาญการทำ function memoization และสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพและตอบสนองได้ดีขึ้นสำหรับผู้ชมทั่วโลก
อย่าลืมทำการโปรไฟล์ (profile) แอปพลิเคชัน React ของคุณเสมอเพื่อระบุปัญหาคอขวดด้านประสิทธิภาพ และใช้ useCallback
(และเทคนิคการเพิ่มประสิทธิภาพอื่นๆ) อย่างมีกลยุทธ์เพื่อแก้ไขปัญหาคอขวดเหล่านั้นอย่างมีประสิทธิภาพ