如何搭配useMemo與useCallback優化React效能
當我們設計Component一段時間之後,Component會隨著時間不斷的壯大,到最後就會面臨效能的議題,如果使用者在使用部分Component的時候,需要花過多的時間載入,一定會影響到使用者對產品的喜好程度。因此以下討論優化Component的思路。
React.memo
在學習useMemo與useCallback之前,讓我們先來認識什麼是React.memo。
官方的介紹為:
React.memo 是一個 higher order component。
如果你的 function component 每次得到相同 prop 的時候都會 render 相同結果,你可以將其包在 React.memo 之中,透過快取 render 結果來在某些情況下加速。這表示 React 會跳過 render 這個 component,並直接重用上次的 render 結果。
簡單來說,你可以把React.memo想像成是一個cache,只有偵測到props改變才會render。
如果你在child Component寫一個log測試render的次數,你可能會發現光是一個click事件就render了好幾次,但是事實上你的props的值是一樣的,再次render反而會耗費額外的效能在客戶端上。
以下面的例子為例:
1 | const App = () => { |
1 | const Card = ({ title }) => { |
當click事件被執行後,App會進行re-render的動作,所以Card也會進行render的動作。
可是事實上Card的props並沒有改變,裡面的結構也沒有改變的必要。
因此我們可以針對Card進行render的優化:
1 | const Card = ({ title }) => { |
使用HOC很簡單,只要簡單的包起來就能擁有特性。
包好之後,你會發現點擊App上的button,Card並不會被re-render了。
但是…如果今天App是長這樣呢?
1 | const App = () => { |
你會發現你觸發Click事件之後,Card竟然開始re-render了!!
你可能心想:傳過去的明明就是const呀!為什麼會re-render?
因為memo只會進行shallow compare,再加上請回想JS的特性
再加上tags他本身是一個物件,每次產生都會分配到不同的記憶體位置,導致memo認為兩者是不相同的。
方法一
不過memo提供第二個參數使用,可以傳送一個compare function進行你要的運算:
1 | const areEqual = (prevProps, nextProps) => { |
Demo
See the Pen React.memo using compare function Example by YangYang (@yyisyou) on CodePen.
方法二
其實只要把不會更動的const放到component外面即可,這樣App re-render的時候,才不會把tags重新分配一個新的記憶體位置。
1 | const tags = ['happy', 'sad', 'madness']; |
Demo
See the Pen React.memo Example by YangYang (@yyisyou) on CodePen.
useMemo
useMemo是React hooks提供的一個更便捷的一種memo方式,它可以監聽特定的資料是否改變,再去改變指定的資料,如此一來可以達到和React.memo想要達成的效果。
1 | const App = () => { |
第一個參數放置的一個回傳需要執行的function
第二個參數是,當特定的值改變的話,將會執行useMemo的動作,以這個例子因為沒有相依性,所以可以填寫空陣列,表示只會在一開始執行一次。
Demo
See the Pen useMemo Example by YangYang (@yyisyou) on CodePen.
useCallback
今天如果突然新增一個function用來回傳一個count文案的contentHandler:
1 | const App = () => { |
並且在Card裡面使用useEffect,右邊的相依陣列放空的表示只會在初次render的時候執行一次。
但是你會發現這樣做就會開始發生re-render了:
1 | const Card = ({ title, contentHandler }) => { |
為了區別container的改變和外部事件的改變,因此新增了trigger button。
你會發現當點選了trigger,一樣會發生re-render的狀況。
原因也是和最上面我們在探討tags物件重新產生一樣的道理,由於anonymous function在container中不斷被產生與傳值,因此React.memo在執行shallow compare的時候,會認為與前一次的function是不一樣的。
以一開始的例子,我們用useMemo把array包起來解決了,那這個例子換成function的情況,可以使用useCallback!
1 | const App = () => { |
我們在可能會被使用的function加上useCallback,相依陣列加上count表示只有在count改變的時候才會產生新的function。
因此流程就會確保是:click add -> add count -> count改變,trigger useCallback產生新function -> re-render相依的component
Demo
See the Pen useCallback Example by YangYang (@yyisyou) on CodePen.
useMemo與useCallback比較
useMemo與useCallback的用法很像,使得初學者不容易判斷該用哪個。
如何區分
讓我們來把上面的範例抓下來看一下(為了方便閱讀,把縮排調整了一下):
1 | const tags = useMemo(() => { |
區分兩者最大的不同在於
- useMemo回傳的是variable
- useCallback回傳的是function
輸出之後會變成
- This is useMemo what looks like: (3) [“happy”, “sad”, “madness”]
- This is useCallback what looks like: () => I did ${count} times in my current job.
看出差別了吧?由於兩者回傳的東西不同,因此使用情境也可能不太相同。
Demo
See the Pen Different between useMemo and useCallback Example by YangYang (@yyisyou) on CodePen.
小結
自從hooks被推出之後,讓React開發更能仰賴props change大過於state change來開發Component。
因此children在效能的議題上,更能focus在props與UI的呈現上。
希望這篇有解答到一些人對於效能優化的疑惑,雖然useMemo與useCallback已經推出一段時間了,不過還是想貢獻一下時間讓更多人釐清觀念上的差異。
如果有其他問題,歡迎一起交流~
友站推薦
我有一位學弟先前撰寫了類似的文章,如果這篇文章無法解決你的問題的話,可以到以下連結觀看:
📌 React 性能優化那件大事,使用 memo、useCallback、useMemo