iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 14

30天React練功坊-攻克常見實務/面試問題 Day14: Optimization with React.memo the wrong way

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天看了一個不必要re-render造成的效能問題並利用React.memo來解決,到這邊一切相安無事,那麼昨天的文章也許會給你一種感覺。
我他媽還不用爆React.memo?
如果這類的想法有閃進你的腦袋,那不好意思今天我要戳破那個美好的想像泡泡了,馬上來看一個例子吧!

本日題目

首先老樣子請你觀察這個codesandbox以及下方的gif檔案。

day14-demo-gif

今天的情境是這樣的,自從學到了React.memo後,該名工程師就把所有的組件都套上這個HOC,寫出了以下的程式碼,頁面上的結構很單純,有著一個按鈕與一個大組件LargeList,這個組件會接受來自大組件的props、也就是一個item list並渲染出許多個小組件SmallComponent,原本預期這樣寫就可以解決當App因為state更新而造成LargeList不必要的重新渲染,但正如gif所展示的情況,更新App的state仍舊造成LargeList重新渲染並也理所當然地讓子組件SmallComponent重新渲染,請試著解釋為什麼這個例子中React.memo並沒有效果。

const SmallComponent = memo(({ item }) => {
  console.log("Rendering SmallComponent");
  return <div>{item.name}</div>;
});

const LargeList = memo(({ items }) => {
  console.log("Rendering LargeList");
  return items.map((item, index) => <SmallComponent key={index} item={item} />);
});

export default function App() {
  const [count, setCount] = useState(0);

  const items = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }];

  return (
    <div>
      <h1>Optimization with React.memo the wrong way</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment Count: {count}
      </button>
      <LargeList items={items} />
    </div>
  );
}

解答與基本解釋

接續昨天的進度,我們今天繼續來討論React.memo,若你真的有看昨天的文章並去補課,我想今天的題目對你來說再簡單也不過了,實際上這份程式碼犯的錯誤可不止在LargeList中。而這類的情況在實務中層出不窮,React為了優化效能而有了許多的工具讓開發者使用,像是這兩天討論的React.memo,或是幾個常見的hooks例如useMemo、useCallback等,他們核心的目標都是為了避免不必要的重複操作,用得好的話自然沒什麼問題,但若是你並沒有妥善地使用,很多時候你都是在扯後腿罷了,因為比對props這樣的行為是需要成本的

我們昨天有講過React.memo會先確認傳入的props是否有更新,若沒有更新的話就skip渲染的過程,而判斷是否有更新的方式則是淺比對(shallow comparison),也就是說如果是物件的話,它單純就是看該物件的reference是否仍是一樣,聽起來是不是有點熟悉,沒錯!我們在dependency array時也講過react是如何判斷更新的,當你想通之後一切其實都是相同的,今天的問題在於我們item的寫法

const items = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }];

<LargeList items={items} />

每一次的重新渲染都會完整地跑過組件內的程式碼,這其中自然也包括函數與變數的宣告,因此雖然變數名都叫items,但實際上對react來說你是每一次都建立了一個全新reference的物件然後傳給LargeList,因此每一次對LargeList來說props都在變動,自然就不會跳過渲染,最終造成你看到的狀況。

想解決的方法也很簡單,假設你真的因為某種原因需要宣告這類固定值的陣列,大致上有兩種常見的手段

  1. 宣告在組件外

只要宣告在組件外,那麼他就不屬於渲染過程的一部分,自然也不會重新宣告,這麼一來reference就會保持相同讓React.memo如你所想的運作。

  1. 利用useMemo

另一種做法我個人較為不推薦,但使用useMemo確實可以解決這個問題,原理有些類似,我們可以利用一個空的dependency array讓useMemo不會重新呼叫,那麼就一直可以保持相同的reference。

const items = useMemo(
  () => [
    { name: "apple" },
    { name: "banana" },
    { name: "cherry" }
  ],
  [] // Dependency array is empty, so items won't be recreated
);

再次強調我並不推薦在這個情況下使用useMemo,原因在於這次的陣列相當單純,並不是個非常吃效能的陣列,這就跟胡亂使用React.memo有著異曲同工之妙。

總結

我們今天看了一個非常常見(是的,至少我看過不少實務上的例子)誤用React.memo的例子,沒搞清楚使用情境導致了react去做沒意義的比對,反而讓效能更差。你僅應該在以下兩種情況都滿足時去使用React.memo,其於絕大多數時間請你離他越遠越好。

1. 組件渲染的成本很高
2. 該組件會頻繁的重新渲染

追求效能優化並不是壞事,但不正確的手段只會將事情越搞越糟,在做任何的優化之前永遠記得去設benchmark,確保自己真的有提升你網站的效能,這是個非常深的坑,我自己也僅僅摸到皮毛而已!希望今天也讓你有所收穫,我們明天見吧!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day13: ExpensiveComponent re-render causing performance issue
下一篇
30天React練功坊-攻克常見實務/面試問題 Day15: Unintended Re-renders: The Pitfalls of useContext
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言