React | 關於 Component 效能優化的那件小事

神Q超人
Starbugs Weekly 星巴哥技術專欄
8 min readFeb 3, 2020
Photo by David Travis on Unsplash

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 每一次都重複 Render

像上方 Title 那種情況就被稱作「沒有必要的渲染」,那該怎麼去避免這件事情呢?在 Class Component 時代還可以繼承 PureComponent 或是用 shouldComponentUpdate 來處理,那 Function Component 呢?

很簡單,這個官方文件在 Hooks FAQ 中就有寫了:

資料來源:https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-shouldcomponentupdate

只需要在 Component 外包上 React.memo 就可以搞定了:

它就不會在 Props 不變的狀況下重新 Render 了:

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 重新產生新的 memoizedTitle 也就會一直重新 Render 啦!

useCallback

最後如果我們想要將點了會加一的按鈕封裝成 Component AddCountButton,並傳入 addCount 的 function 進去,而我們也為了防止「沒有必要的渲染」,沒忘了替 AddCountButton 加上 React.memo:

讓我們看看執行時的 Render 狀況:

AddCountButton 每一次都重新 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:

我們把 CounteraddCount 改掉,來解決這個問題:

最後再看一下是否順利執行:

不論怎麼加一都再也不會重新 Render AddCountButton了

在成功執行的當下,我們也已經學成了如何有效的減少「不必要的 Render」 來提高網頁效能了 🎉

但是要注意哦!雖然以上三個方法可以輕鬆的將優化效能,但是在 Kent C. Dodds 的「When to useMemo and useCallback」這篇文章中裡面提到,因為這幾個優化的方法都會為原本的程式碼增加一定的複雜度,所以在使用時請先想想

現在加入這些優化到底是真的優化,或只是在增加成本呢?

那上方文章中的例子都有整理在 GitHub 上了,歡迎大家直接 clone 玩玩 🙌

進入到 Hooks 時代後處理效能真的變得非常簡單,只要掌握 React.memo、useMemo、useCallback 就沒什麼好怕的,建議大家可以試著把這三種方法都實作一次,相信會更了解他們的使用方式和情境。

如果文章裡有任何問題或是錯誤的地方,再請麻煩留言告訴我,我會盡快改進,非常感謝 🙇‍♂️

延伸閱讀

  1. https://kentcdodds.com/blog/usememo-and-usecallback

參考文章

  1. React 性能優化大挑戰:一次理解 Immutable data 跟 shouldComponentUpdate
  2. 如何錯誤地使用 React hooks useCallback 來保存相同的 function instance

--

--