การใช้งานและความแตกต่างระหว่าง useMemo และ useCallback ของ React Hooks
Table of contents
Intro
สวัสดีครับ บทความนี้จะแนะนำแนวทางการใช้งาน React Hooks ที่ชื่อ useMemo และ useCallback คืออะไร มีความสำคัญอย่างไร ช่วยเพิ่มประสิทธิภาพลดการ Render ของ React ที่หนักหน่วงอย่างไร ควร Refactor อย่างไร มาดูกันเลยครับ 😍
TL; DR;
- useMemo คือการ Cache ค่า Value ถูกเรียกครั้งแรกเมื่อมีการ Render และครั้งต่อไปเมื่อมีการ Re-Render
และ
ค่าใน Array deps มีการเปลี่ยนแปลง โดย Return ออกเป็นค่า Value - useCallback คือการ Cache Function
ไม่
ถูกเรียกครั้งแรกเมื่อมีการ Render โดยจะถูกเรียกก็ต่อเมื่อสั่ง Call ฟังก์ชันและ
ค่าใน Array deps มีการเปลี่ยนแปลง โดย Return ออกเป็น Function
เริ่มกันเลย
ปัญหา Re-Render
ปัญหาการ Re-Render นั้นเป็นปัญหาใหญ่ ที่ทำให้เกิดการ Render ที่หนักหน่วง ตามปกติทุกการ Render จะเกิดการจอง Memory (Memory allocation) และบางทีการ Render Component ซ้ำ ๆ เกินความจำเป็นอาจจะทำให้เกิดการจัดสรรหน่วยความจำอย่างไม่ถูกต้อง (Memory Leak) ได้
แก้ปัญหาด้วย useMemo
ในตัวอย่างนี้จะใช้การวิธีการแสดงค่า Timestamp ของฟังก์ชัน Date เป็นกรณียกตัวอย่างเมื่อหน้าเว็บถูก Re-Render ในแต่ละครั้ง
() => { const [number, setNumber] = useState(0) const [someValue, setSomeValue] = useState(0) const getTimestamp = () => (new Date().getTime()) const numberWithoutMemo = getTimestamp() const getNumberWithMemo = useMemo(() => { return getTimestamp() }, [someValue]) return ( <> <p>แบบไม่ใช้ useMemo: {numberWithoutMemo}</p> <hr /> <p>แบบใช้ useMemo: {getNumberWithMemo}</p> <hr /> ทดลองเปลี่ยนค่า (Number): {number}<br/> <button onClick={() => { setNumber((prev) => ++prev) }}>เพิ่ม (+)</button> <button onClick={() => { setNumber((prev) => --prev) }}>ลด (-)</button> <hr /> ทดลองเปลี่ยนค่า (Some value): {someValue}<br /> <button onClick={() => { setSomeValue((prev) => ++prev) }}>เพิ่ม (+)</button> <button onClick={() => { setSomeValue((prev) => --prev) }}>ลด (-)</button> </> ) }
แบบไม่ใช้ useMemo
- Timestamp แสดงครั้งแรกเมื่อมีการ Render
- Timestamp แสดงอีกครั้งเมื่อมีการ Re-Render (Number หรือ Some Value เปลี่ยนแปลง)
- เป็นตัวอย่างกรณีการ Re-Render เกินความจำเป็น
แบบใช้ useMemo
- Timestamp แสดงครั้งแรกเมื่อมีการ Render
- Timestamp แสดงอีกครั้งเมื่อมีการ Re-Render และ ค่า Some Value มีการเปลี่ยนแปลงเท่านั้น
- เป็นตัวอย่างกรณีการ Re-Render เฉพาะที่จำเป็น
แก้ปัญหาด้วย useCallback
ในตัวอย่างนี้จะใช้การวิธีการแสดงค่า Timestamp ของฟังก์ชัน Date เป็นกรณียกตัวอย่างเมื่อหน้าเว็บถูก Re-Render ในแต่ละครั้ง
() => { const [number, setNumber] = useState(0) const [someValue, setSomeValue] = useState(0) const [numberWithCallback, setNumberWithCallback] = useState(undefined) const getTimestamp = () => (new Date().getTime()) const numberWithoutCallback = getTimestamp() const getNumber = useCallback(() => { setNumberWithCallback(() => getTimestamp()) }, [someValue]) return ( <> <p>แบบไม่ใช้ useCallback: {numberWithoutCallback}</p> <hr /> <p>แบบใช้ useCallback: {numberWithCallback}</p> <hr /> ทดลองเปลี่ยนค่า (Number): {number}<br /> <button onClick={() => { setNumber((prev) => ++prev) }}>เพิ่ม (+)</button> <button onClick={() => { setNumber((prev) => --prev) }}>ลด (-)</button> <hr /> ทดลองเปลี่ยนค่า (Some value): {someValue}<br /> <button onClick={() => { setSomeValue((prev) => ++prev) getNumber() }}>เพิ่ม (+)</button> <button onClick={() => { setSomeValue((prev) => --prev) getNumber() }}>ลด (-)</button> </> ) }
แบบไม่ใช้ useCallback
- Timestamp แสดงครั้งแรกเมื่อมีการ Render
- Timestamp แสดงอีกครั้งเมื่อมีการ Re-Render (Number หรือ Some Value เปลี่ยนแปลง)
- เป็นตัวอย่างกรณีการ Re-Render เกินความจำเป็น
แบบใช้ useCallback
- Timestamp ไม่แสดงครั้งแรกเรียกเมื่อมีการ Render (จะแสดงเมื่อเราสั่ง Call ฟังก์ชันเอง)
- Timestamp แสดงเมื่อสั่ง Call ฟังก์ชัน และ ค่าใน Some Value มีการเปลี่ยนแปลงเท่านั้น
- เป็นตัวอย่างกรณีการ Re-Render เฉพาะที่จำเป็น
สรุป
หลักการของ useMemo คือการ Cache ข้อมูลไว้
- ถูกเรียกครั้งแรกเมื่อมีการ Render
- ถูกเรียกอีกครั้งเมื่อมีการ Re-Render
และ
ค่าใน Array deps มีการเปลี่ยนแปลง - Return ออกเป็นค่า Value
หลักการของ useCallback คือการ Cache ฟังก์ชันไว้
ไม่
ถูกเรียกเมื่อมีการ Render (จะถูกเรียกเมื่อเราสั่ง Call ฟังก์ชันเอง)- ถูกเรียกอีกครั้งเมื่อสั่ง Call ฟังก์ชัน
และ
ค่าใน Array deps มีการเปลี่ยนแปลง - Return ออกเป็น Function
useCallback(fn, deps) จะเท่ากับ useMemo(() => fn, deps)
ทั้งสองตัวมีการทำงานที่คล้ายกัน และแตกต่างกันนิดหน่อย แต่ทั้งสองสามารถช่วยเพิ่มประสิทธิภาพเพื่อช่วยลดการ Re-Render ในบางกรณีที่เกินความจำเป็นได้ และต้องเลือกใช้ตามความเหมาะสมดังนี้
- เมื่อ Client ใช้ Device สเปคเครื่องไม่ค่อยแรง React อาจจะหน่วงและช้ามาก ๆ ซึ่งการ Optimize render ก็อาจจะช่วยไปได้เยอะมาก ๆ
- useMemo ใช้เมื่อถ้ามีการคำนวณอะไรที่ใช้เวลานานมาก ๆ และต้องการทำ Memorizes ไว้ แต่ในการกลับกัน ไม่ควรใช้กับการคำนวณง่าย ๆ เช่น 1 + 1 เพราะเป็นปกติจะได้ผลลัพธ์เร็วอยู่แล้ว และอาจจะทำให้เกิด Overhead เปล่า ๆ
- เมื่อมี Component ซับซ้อนและเยอะ และมี Logic เยอะ ที่เป็น Parent Child หลายชั้น ถ้าไม่มีการทำ Optimize render อาจจะช้าและค้างได้เลย
- Input Form ที่มีฟิลด์เยอะ ๆ ถ้าไม่คำนึงเรื่องพวกนี้ ถ้าเวลาพิมพ์ เร็ว ๆ จะสะดุดมาก ๆ
รายละเอียดเทคนิคอื่น ๆ
- React ถ้าใช้ useEffect ควรเขียน Cleanup functions วิธีใช้งานแบบผิด ๆ ที่มือใหม่อาจจะพลาด
- เพิ่มประสิทธิภาพลดขนาด App bundle size ด้วย Dynamically Importing กับ React.lazy
- Debouncing คืออะไร ?
- Throttling คืออะไร แตกต่างกับ Debouncing อย่างไร (ตอนจบ)
- บันทึกการใช้งาน useRef createRef และ forwardRef ของ React
- เจาะลึกความลับ Shallow และ Deep Copies ใน JavaScript (ตอนที่ 1)