iamsirid.com
ใช้ React.memo() ให้ฉลาด
Created At: 07-06-2022 02:33
Last Updated: 06-07-2022 12:43

ใช้ React.memo() ให้ฉลาด

หลายคนที่เขียน React คงรู้จักตัว HOC ชื่อ React.memo() ในการทำ Memoization ให้กับ Functional Component กันอยู่แล้ว เพื่อไม่ให้ Component นั้นถูก re-render โดยไม่จำเป็น แต่บางคนอาจจะสงสัยว่าทำไม React ไม่ทำ Memoization ให้ทุก Component ไปเลยโดยจำเป็นต้องมีตัว React.memo() แยก หรือทำไมเราไม่ใช้ React Memo กับทุก Component ไปเลยล่ะ ? ในบล็อกนี้เราจะพูดถึงกรณีที่ควรใช้ React Memo และกรณีที่ไม่ควรใช้ React Memo กันครับ

ทวนเรื่อง Memoization

หลักการ Memoization นั้นเป็นเทคนิคการทำ Optimization ให้โปรแกรมของเราเพื่อให้โปรแกรมทำงานได้อย่างมีประสิทธิภาพและเร็วขึ้น ด้วยการเก็บผลการคำนวณ (Computation results) ไว้ใน cache และดึงผลการคำนวณนี้ออกมาใช้ในครั้งต่อไปที่มีการคำนวณแบบเดิมแทนที่จะต้องคำนวณใหม่

ตัวอย่างการทำ Memoization ในภาษา Javascript

สมมุติเราต้องการหาตัวเลขลำดับที่ nth ใน Fibonacci Sequence เราสามารถเขียน Recursive function ได้ตามนี้

const fib = (n) => { if (n <= 1) return 1; return fib(n - 1) + fib(n - 2); };

สมมุติเราต้องการหาตัวเลขลำดับที่ 5 โดยการเรียก fib(5) ตัวฟังก์ชั่นจะถูกเรียกแบบ recursive ดังแผนภาพนี้

จะสังเกตุได้ว่า fib(0), fib(1), fib(2) and fib(3) ถูกเรียกอยู่หลายครั้ง ทำให้เกิดการคำนวณซ้ำแบบเดิมโดยไม่จำเป็น เราสามารถแก้ปัญหานี้ได้ด้วยการทำ Memoization โดยเปลี่ยนการ implement ตัวฟังก์ชั่น fib เป็นดังนี้

const fib = (n, memo) => { memo = memo || {}; if (memo[n]) return memo[n]; if (n <= 1) return 1; return (memo[n] = fib(n - 1, memo) + fib(n - 2, memo)); };

สังเกตได้ว่าเราจะมีการเพิ่ม memo object ขึ้นมาเพื่อเป็น cache และเมื่อเจอลำดับที่ n ซ้ำ แปลว่า memo[n] ได้ถูกคำนวณไปแล้ว เราก็จะ return ค่าที่เก็บเอาไว้เลยโดยไม่ต้องคำนวณอีกรอบ

หลักการของ React.memo()

React.memo() เป็น Higher order component (HOC)

const MyComponent = React.memo(function MyComponent(props) { /* render using props */ });

โดย Component จะถูก re-render ก็ต่อเมื่อ props มีการเปลี่ยนแปลง นั่นหมายความว่าผลลัพธ์ของการ render ครั้งก่อนจะถูก memonize ไว้และแสดงผลเดิม ถ้า props เหมือนเดิม ซึ่งช่วยในเรื่อง Performance Optimization

ซึ่งการเปรียบเทียบ props ล่าสุดกับ props ก่อนหน้านั้น React จะจัดการให้อัตโนมัติ แต่เราก็สามารถเขียน Compare function ได้เองถ้าต้องการ (เช่น การทำ deep object comparison)

function MyComponent(props) { /* render using props */ } function areEqual(prevProps, nextProps) { /* return true ถ้า nextProps กับ prevProps เปรียบเทียบแล้วเหมือนกัน ถ้าไม่เหมือนกันให้ return false */ } export default React.memo(MyComponent, areEqual);

จะเห็นว่า React.memo() รับ argument ที่ 2 เป็น Compare function ที่ return ค่า boolean ระบุว่า props ล่าสุดกับ props ก่อนหน้าเหมือนกันหรือไม่

เมื่อไหร่ควรใช้ React.memo()

เราควรใช้ React Memo ก็ต่อเมื่อ Component นั้นถูก re-render บ่อย ๆ ด้วย props เดิมซ้ำ ๆ ซึ่งจะเกิดได้บ่อยจากการที่ Component นั้นเป็น Child ของอีก Component นึงที่มีการ re-render บ่อย แต่ส่ง props เดิมให้ Child

ตัวอย่างเช่น สมมุติให้เรามี Movie Component

function Movie({ title, releaseDate }) { return ( <div> <div>Movie title: {title}</div> <div>Release date: {releaseDate}</div> </div> ); }

ซึ่งถูก render เป็น Child ของ MovieViewsRealtime Component

function MovieViewsRealtime({ title, releaseDate, views }) { return ( <div> <Movie title={title} releaseDate={releaseDate} /> Movie views: {views} </div> ); }

โดยสมมุติให้ MovieViewsRealtime นั้นรับ prop ชื่อ views ซึ่งเป็นค่าที่ถูก fetch มาจาก api ทุก ๆ วินาที

<MovieViewsRealtime views={0} title="Forrest Gump" releaseDate="June 23, 1994" /> // After 1 second, views is 10 <MovieViewsRealtime views={10} title="Forrest Gump" releaseDate="June 23, 1994" /> // After 2 seconds, views is 25 <MovieViewsRealtime views={25} title="Forrest Gump" releaseDate="June 23, 1994" />

ซึ่งทุก ๆ ครั้งที่ค่าของ views เปลี่ยน แน่นอนว่า MovieViewsRealtime นั้นจะถูก re-render แต่นั่นทำให้ Movie ถูก re-render ตามไปด้วยเพราะเป็น Child ถึงแม้ props ของ Movie จะไม่เปลี่ยนก็ตาม

กรณีนี้จึงควรใช้ React.memo() ที่ตัว Movie Component

function Movie({ title, releaseDate }) { return ( <div> <div>Movie title: {title}</div> <div>Release date: {releaseDate}</div> </div> ); } const MemoizedMovie = React.memo(Movie);
function MovieViewsRealtime({ title, releaseDate, views }) { return ( <div> <MemoizedMovie title={title} releaseDate={releaseDate} /> Movie views: {views} </div> ); }

กรณีนี้ตัว MemoizedMovie จะถูก re-render ก็ต่อเมื่อ props ของมันเปลี่ยนเท่านั้น (title, releaseDate)

เมื่อไหร่ไม่ควรใช้ React.memo()

กรณีนี้ที่ไม่ควรใช้ React Memo นั้นคือกรณีที่ Component นั้นถูก re-render บ่อย ๆ ด้วย prop ไม่ซ้ำกัน สมมุติให้ MemoizedMovie รับ views prop ด้วย

... <MemoizedMovie views={views} title={title} releaseDate={releaseDate} /> ...

กรณีนี้ MemoizedMovie จะถูก re-render ทุกครั้งที่ views เปลี่ยน ซึ่งทำให้ไม่มีประโยชน์ที่ใช้ React Memo แถมยังทำให้ Performance แย่ลงอีกด้วยเพราะจะเกิด Overhead ที่ Compare function ที่เปรียบเทียบ props ทุกครั้งบ่อย ๆ โดยผลลัพธ์ออกมาว่าต้อง re-render ตลอดอีกด้วย

สรุป

สรุปการใช้ React Memo ได้จาก Rule of thumb ที่ว่า

Don't use memoization if you can't quantify the performance gains.

คืออย่าใช้ React Memo ไปหมดทุก Component ถ้าลองแล้วไม่ได้ช่วยให้ Performace ดีขึ้น เพราะนอกจากจะไม่ดีขึ้นแล้วยังทำให้ Performance ตกอีกด้วย

อ้างอิง

https://www.freecodecamp.org/news/memoization-in-javascript-and-react/

https://reactjs.org/docs/react-api.html#reactmemo

https://dmitripavlutin.com/use-react-memo-wisely