React | 關於 Component 效能優化的那件小事
Hi!大家好,我是神 Q 超人。這裡先和大家說聲新年快樂!打這篇文章的當下是 1/25(是不是很想回到那一天 😂),新的一年總要有個目標,難得放了那麼長的假,前兩天秒速破完寶可夢劍盾後,不如再花點時間來研究一下 React 中的效能優化吧!(這有什麼關聯 😂)
關於效能這件事情其實我一直想研究很久了,去年也看過滿多文章,其中講得很清楚的就是「React 性能優化大挑戰:一次理解 Immutable data 跟 shouldComponentUpdate」,但現在 React 進入大 Hooks 時代了,Hooks 問題要用 Hooks 手段,本篇文章的內容會提到 React.memo、useCallback 和 useMemo,讓我們來輕鬆了解效能優化!
React.memo
首先讓我們先上一道 Component,叫做 Counter
:
對 React 來說啊,只要 Counter
的 State 改變,那它裡面的 DOM 就全都會被重新 Render,就算是沒有任何改變的 Title
也是一樣,因為它在 Counter
裡面:
像上方 Title
那種情況就被稱作「沒有必要的渲染」,那該怎麼去避免這件事情呢?在 Class Component 時代還可以繼承 PureComponent 或是用 shouldComponentUpdate 來處理,那 Function Component 呢?
很簡單,這個官方文件在 Hooks FAQ 中就有寫了:
只需要在 Component 外包上 React.memo 就可以搞定了:
它就不會在 Props 不變的狀況下重新 Render 了:
但是要注意哦!React.memo 只會自動檢查 Props,不會管 State!
然後其實 React.memo 可以接收第二個 CallBack 函式,只要回傳 false
就會 Render,true
就不會,和之前的 shouldComponentUpdate 相反,然後它只能取到 Props 來判斷,沒有 State。這個部分就不演示了,不過大家可以試試看下方的程式碼:
useMemo
看來我們已經做到了防止「沒有必要的渲染」的第一步了,接下來假設 Title
的內容是需要從外面傳進去的,然後用 Props 傳進去的內容是 Object,像是這樣子:
非常好,雖然我們用了 React.memo 為 Title
判斷了 Props 相同就不需要再重新 Render,但遺憾的是在 JavaScript 中,Object 這種東西會因為 reference 的關係,不會被正確的判斷到是否相等,像是:
{} === {} // false
所以在上面那段程式碼中,Title
就算每一次的 Props 都相同,還是會被瘋狂 Render,該怎麼辦才好?
就用 useMemo 吧!一個基本的 useMemo 長這樣子:
useMemo 會接收兩個參數,第一個參數是函式,該函式回傳的內容會是一個memoized,它會把回傳的 { name: ‘計數器’ }
存放在 titleContent
,而第二個參數是一個 Array,只要該 Array 內的變數值沒有改變,那 useMemo 就不會重新產生一個 memoized,既然不會重新產生,那 Title
也就不會重新 Render 了!然後上方的 Array 是空的,也就代表不管在任何情況下,這個 useMemo 都不會重新產生 memoized。
下方讓我們把 useMemo 加到 Counter
中:
結果就不一樣囉!這裡我就不再另外展示了,但絕對不是我想偷懶的關係,是因為就算再展示,結果也和上方的 Gif 一樣 😂
另外,如果大家想要體會看看 useMemo 的第二個參數,可以將 count
加到 Array 裡,這樣子在每一次點擊按鈕的時候, count
就會改變,導致 useMemo 重新產生新的 memoized,Title
也就會一直重新 Render 啦!
useCallback
最後如果我們想要將點了會加一的按鈕封裝成 Component AddCountButton
,並傳入 addCount
的 function 進去,而我們也為了防止「沒有必要的渲染」,沒忘了替 AddCountButton
加上 React.memo:
讓我們看看執行時的 Render 狀況:
歐買尬,為什麼會發生這種事情?React.memo 也有加上了不是嗎?原因其實就出現在那個 function 身上,如果說 useMemo 是為了對抗 reference 而存在,那 useCallback 就是為了 function。
useCallback 的基本用法如下:
它和 useMemo 其實一模一樣,只差在 useCallback 會回傳整個 function 的 memoized,而不是回傳值,至於第二個參數的 Array 就和 useMemo 相同,這裡就不再多說了。我們將 useCallback 加到 Counter
中:
這時候!讓我們看看發生什麼事情:
AddCountButton
雖然沒有再 Render 了,但是 count
也跟著靜止不動,為什麼呢?因為 addCount
這個事件被 useCallback 處理掉了啊!它被包起來了,所以在 Counter
重新 Render 的時候 addCount
也不會產生一個新的 memoized,導致一開始的這個 function:
() => { setCount(count + 1); }
它裡面的 count
一直都是最初的 0,這時候 0 不管再怎麼加 1 也都還是 1,才會顯得畫面好像都沒有變化。
所以怎麼辦呢?這裡我從「如何錯誤地使用 React hooks useCallback 來保存相同的 function instance」這篇文章(裡面也有對這個情況作解釋)看見作者解決的方式,他利用 setState
能夠傳入一個 function,然後這個 function 會接收到一個參數,這個參數就是目前 State 的特性來處理這件事,而 function 的回傳值就會直接更新 State:
我們把 Counter
的 addCount
改掉,來解決這個問題:
最後再看一下是否順利執行:
在成功執行的當下,我們也已經學成了如何有效的減少「不必要的 Render」 來提高網頁效能了 🎉
但是要注意哦!雖然以上三個方法可以輕鬆的將優化效能,但是在 Kent C. Dodds 的「When to useMemo and useCallback」這篇文章中裡面提到,因為這幾個優化的方法都會為原本的程式碼增加一定的複雜度,所以在使用時請先想想
現在加入這些優化到底是真的優化,或只是在增加成本呢?
那上方文章中的例子都有整理在 GitHub 上了,歡迎大家直接 clone 玩玩 🙌
進入到 Hooks 時代後處理效能真的變得非常簡單,只要掌握 React.memo、useMemo、useCallback 就沒什麼好怕的,建議大家可以試著把這三種方法都實作一次,相信會更了解他們的使用方式和情境。
如果文章裡有任何問題或是錯誤的地方,再請麻煩留言告訴我,我會盡快改進,非常感謝 🙇♂️
延伸閱讀
參考文章