การใช้งานและความแตกต่างระหว่าง 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)