---
# System prepended metadata

title: '[筆記] React 效能優化'
tags: [效能優化, React]

---

# [筆記] React 效能優化

# React 的 render 原理

react 具有 virtual DOM 機制，來處理 CSR 因為資料變更需要頻繁操作 DOM 而導致瀏覽器需要重複 reflow&repaint 的問題，這個過程相當耗費計算資源。

virtual DOM 是 React 用來記錄 DOM tree 上有那些元素、元素分別為什麼狀態的 Object，當 state 改變， React 會透過 diff 演算法，把新舊 virtual DOM 互相比較、篩選出有更動的部分，針對那個部分去進行DOM修改。

所以觸發流程會是長這樣：

```mermaid
graph LR
C[Component] --React--> Vir[Virtual DOM] --diff--> Ch[Change part]--ReactDOM--> D[DOM]
```

其中最耗效能的部位會在：

1. diff 演算法
2. ReactDOM 修改 DOM

由此可知，若要優化效能，我們需要盡量避免：

1. **觸發元件 re-render** → 避免觸發 diff 演算法
2. **盡量保持 virtual DOM 的一致** → 避免觸發 ReactDOM 修改 DOM

# **React 是如何決定 re-render 元件的**

其實比想像中簡單，react 預設就是:

> **當 state 被更動或接受到新的 props，就會觸發 re-render 元件**
> 

但這個簡單的原理，碰上 JavaScript 的特性，就不這麼簡單了。

我們都知道，JavaScript 有 `by value` 和 `by reference` 的資料型別差異，前者稱為 **Primitive type (原始型別)**，後者稱作 **Object Type(物件型別)/reference Type(參考型別)**。

React Component 會針對 state 內的值去做嚴格相等(`===`)，判斷是否需要 re-render。

當 state 是**原始型別**時，因為比較的是值，所以值不相等才會觸發 re-render。

而若是 object 這類型的**參考型別**，比較的則是 reference，也就是該物件在記憶體中的地址，但由於每一次創建 object 時，都會給定一個新的地址，基本上只要有新的 object 傳入 `setState`，不論裡面的值是否跟上次相同，都會觸發 re-render。

```jsx
//原始型別狀況
const oldState = 1;
const newState = 1;
oldState === newState; //true

//參考型別狀況
const oldState = {id: 1};
const newState = {id: 1};
oldState === newState; //false
```

而為了要觸發 `setState`，我們必須傳入新物件，才可以觸發 `state` 更新，在這種狀況下，無論你的 object 值是否相同，都會因為 reference 改變而觸發 re-render。

而當父層元件 re-render ，子層元件因為是父層元件的一部份，所以即便子層的 props 沒有變動，也一樣會 re-render，例如下面的例子：

```jsx
const Content = () => <div>I'm Content!</div>

const App = () => {
  const [state, setState] = useState()
  const onClick = () => setState(!state)

  return (
    <>
      <button onClick={onClick}>change!</button>
      <Content /> {/* Content 沒有 props 但也一樣會 rerender*/}
    </>
  )
}
```

# React.memo、useMemo、useCallback

理解了 React 的 render 機制後，React 也提供了幾種方法來避免多餘且昂貴的 re-render。

下面是一個常見的 React 範例，由 App 包裹多個元件，並在這邊控制 state，將 state 傳入子元件的 props，請問：

> 按下 `button` 後，更新 `otherState`，`Table` 是否會被 re-render? [^ReactQuiz]
> 

```jsx
const Row = ({item, style}) => (
  <tr style={style}>
    <td>{item.name}</td>
  </tr>
);

const Table = ({list}) => {
  const itemStyle = {
	color: 'red'
  };
  return (
    <table>
      {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)}
    </table>
  )
};

const App = () => {
  const defaultList = Array(10000).fill(0).map((val, index) => ({name: index}));
  const [list, setList] = useState(defaultList);
  const [otherState, setOtherState] = useState(0);

  const onClick = () => {
    setOtherState(1)
  };

  return (
    <>
      <Table list={list} />
      <button onClick={onClick}>change state!</button>
    </>
  )
};
```

- 答案是：
    
    會！即便 Table 沒有傳入任何 props ，Table 也會因為 App re-render 而導致 re-render。
    

## React.memo

上述的 `Table` 例子，照理來說因為 props 沒有變化，所以不需要 re-render，React 提供了 `React.memo` 這個方法，來避免這種情形。

`React.memo` 類似 class component 的 pure component，此方法會回傳一個 higher order component，會比較傳進來的 props 是否跟前次呼叫時相同，比較方式則採取淺拷貝(shallow copy)。

使用 React.memo 改寫過後會長這樣：[^ReactQuiz]
```jsx
const Row = ({item, style}) => (
  <tr style={style}>
    <td>{item.name}</td>
  </tr>
);

const Table = React.memo(({list}) => {
  const itemStyle = {
    color: 'red'
  };
  return (
    <table>
      {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)}
    </table>
  )
});

const App = () => {
  const defaultList = Array(10000).fill(0).map((val, index) => ({name: index}));
  const [list, setList] = useState(defaultList);
  const [otherState, setOtherState] = useState(0);
  const onClick = () => {
    setOtherState(1)
  };

  return (
    <>
      <Table list={list} />
      <button onClick={onClick}>change state!</button>
    </>
  )
};
```

這樣就可以阻止 `Table` 被 re-render了。

那如果換成下面這個例子，點擊 `button` 後的確會改變 `list` 狀態，此時因為 `list` 的確發生變化，要繼續優化的話，我們需要把 `React.memo` 換成包裹住 `Row`。

下一個問題來了：[^ReactQuiz]

> 當 `Table` re-render 時，使用 `React.memo` 包裹的 `Row` 是否達到優化效果?
> 

A. 是，有 `React.memo` 的 `Row` 比 沒有的 `Row` 效能更好

B. 否，`React.memo` 包裹的 `Row` 和 沒有的 `Row` 效能差不多

C. 否，`React.memo` 包裹的 `Row` 比起沒有的 `Row` 效能更差

```jsx
const Row = React.memo(({item, style}) => (
  <tr style={style}>
    <td>{item.name}</td>
  </tr>
));

const Table = ({list}) => {
  const itemStyle = {
    color: 'red'
  };
  return (
    <table>
      {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)}
    </table>
  )
};

const defaultList = Array(10000).fill(0).map((val, index) => ({name: index}));

const App = () => {
  const [list, setList] = useState(defaultList);
  const [otherState, setOtherState] = useState(0);
  const onClick = () => {
    setList(prev => [...prev, {name: 123}])
  };

  return (
    <>
      <Table list={list} />
      <button onClick={onClick}>change state!</button>
    </>
  )
};
```

答案是：C，但你知道為什麼嗎?

### 問題出在 item ?

上面的例子，我們確定 `Table` 必定會 re-render，因為 list 添加了一個值，上面使用了解構賦值，所以我們知道，`list` 的 reference 必定會改變，那 `list` 中的 `item` 也是一個 object，`item` 的 reference 也會改變嗎?

答案是：不會！因為解構賦值本身是淺拷貝，第二層的 object reference 並不會改變。

所以可以確定這邊的 `item` 跟 `item.name` 都不會變動，那問題會出在哪呢?

```jsx
const Table = ({list}) => {
  const itemStyle = {
    color: 'red'
  };
  return (
    <table>
      {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)}
    </table>
  )
};

const App = () => {
  const defaultList = Array(10000).fill(0).map((val, index) => ({name: index}));
  const [list, setList] = useState(defaultList);
  const [otherState, setOtherState] = useState(0);

  const onClick = () => {
    setList(prev => [...prev, {name: 123}])
  };

  return (
    <>
      <Table list={list} />
      <button onClick={onClick}>change state!</button>
    </>
  )
};
```

### 問題出在 itemStyle?

你可能會想：怎麼可能！`itemStyle` 是一個寫死的 object ，我連動都沒有動，怎麼可能會造成 re-render？但他的問題就是出在，**`itemStyle` 是個 object**。

當 `Table` re-render 時，定義在 `Table` 作用域的 `itemStyle` 也會被**重新定義。**也就是說，`itemStyle` 即便內容一樣，但他還是換了一個新地址。

這就導致舊的 `Row` 傳入的 `itemStyle` 通通跟之前不一樣，`React.memo` 也會認為 props 的確更新了，進而導致沒有變化的 `Row` 通通 re-render 的情況。

前面已提到，diff 演算法是拖累 React 效能的禍首之一。在這個例子下，`React.memo` 不但沒有達到優化的效果 (有用沒用都一樣會 re-render)，反而因為加了 `React.memo` ，React 還必須比較前後 props 有無差異，結果**優化不成反而還加重效能負擔**。

```jsx
const Table = ({list}) => {
  const itemStyle = {
    color: 'red'
  };
  return (
    <table>
      {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)}
    </table>
  )
};
```

另外也要注意的是，function 也是 reference type，他也會讓元件重渲染時，導致子元件的 `React.memo` 失去作用：

```jsx
const Btn = ({label}) => {
 /* re-render 就會重新定義 onClick */
  const onClick = () => {
    /* do something... */
  };
  return (
    <Button onClick={onClick}>
      {label}
    </Button>
  )
};

const Btn = ({label}) => {
  return (
   /* 寫在 inline 也會重新定義 onClick function */
    <Button onClick={() => {/*do somthing*/}}>
      {label}
    </Button>
  )
}
```

## useMemo & useCallback

先說結論：

> 90% 的情況下你的專案都不需要使用 useMemo & useCallback [^useMemo&Cb]

這兩個 Hook 也是 React 針對效能優化所提供的功能，但他們跟 `React.memo` 不一樣的是，他們是針對 **昂貴的計算結果/function** 進行緩存，只有當 dependency 有所變化時，才會再進行一次重新計算。

**所以 useMemo 跟 useCallback 並不是用來阻止元件 re-render 的！**

你可能會說，不對阿，那我用 useMemo 把值存起來，再傳到子元件裡，下次 re-render 時，props 就不會變動，那元件不就不會 re-render 了嗎?

No, No, No，要是這樣我就不用寫這麼多了~

根據上面這個假設，你的 code 大概會長的像這樣：

```jsx
// 1. 利用 useCallback 記憶 onClick 想避免 button re-render
const Component = () => {
  const onClick = useCallback(
    () => {    /* do something */  }, []
  );
  return (
    <>
      <button onClick={onClick}>Click me</button>
     ... // 其他元件  
    </>  
  );
};

// 2. useCallback 依賴 value，所以為了避免 onClick 重新被定義，value 使用 useMemo 緩存
const Item = ({ item, onClick }) => <button onClick={onClick}>
  {item.name}
</button>;
const Component = ({ data }) => {
  const [someStateValue, setSomeStateValue] = useState();
  const value = useMemo(() => ({ a: someStateValue }), [someStateValue]);
  const onClick = useCallback(() => {    console.log(value);  }, [value]);
  return (
    <>
      {data.map((d) => (<Item item={d} onClick={onClick} />))}
    </>
  );
};
```

如果你曾經幹過這種事，恭喜你，上述的優化**通通沒作用**！不只如此，還因為多了一堆比較跟緩存拖慢效能，code 也變得更複雜難解。

上述範例的邏輯建立在下面這個前提：

> **只要 props 不改變，元件就不會re-render**
> 

嘿！還記得前面說 React 怎麼觸發元件 re-render 嗎？我們再來複習一下：

> **當 state 被更動或接受到新的 props，就會觸發 re-render 元件**
> 

React 只說了這個情況會觸發 re-render，但是這並不代表，只要 props 不改變，元件就不會 re-render。上面這個假設，實際上是落入了典型的邏輯錯誤， A 是 B 的充分條件，但 !A 不代表是 !B 的充分條件[^useMemo&Cb]。

事實上，只要碰上了**父元件 re-render**，不管你的 props 有沒有改，甚至是沒有 props ，**子元件通通都得 re-render**！

而 React 提供真正能阻止子元件 re-render 的方法只有 `React.memo` 。父元件渲染子元件時，若碰到 `React.memo` 才會中斷子元件的 render，確定 props 有修改，才會繼續 render。

所以 `useMemo` 跟 `useCallback` 若要真正阻止子元件重新渲染，必須搭配 `React.memo`，就像這樣：

```jsx
// React.memo 會中斷 re-render 程序，判斷是否需要繼續渲染
const Item = React.memo(({ item, onClick }) => <button onClick={onClick}>
  {item.name}
</button>);
const Component = ({ data }) => {
  const [someStateValue, setSomeStateValue] = useState();
  // value 是 onClick 的依賴項，所以也必須緩存
  const value = useMemo(() => ({ a: someStateValue }), [someStateValue]);
  // onClick 必須被緩存，否則每次 Component 重渲染 onClick 就會被重新定義，導致 React.memo 無作用
  const onClick = useCallback(() => {    console.log(value);  }, [value]);
  return (
    <>
      {data.map((d) => (<Item item={d} onClick={onClick} />))}
    </>
  );
};
```

也只有在搭配 `React.memo` 的狀態下，使用 `useCallback` 與 `useMemo` 來做 props 的緩存才有意義。**但是你做這樣子的優化付出的代價也相當巨大**。

前面也有提到，React 最花效能的地方是在 **diff 演算法**及 **render DOM** 的步驟，大部分的情況下，記憶這些數值/function 相比之下並沒有太大的優化效果。

大量運算大多會集中在迴圈相關的操作，[這篇文章](https://cloud.tencent.com/developer/article/1967299)針對 useMemo 能優化多少效能做了實驗。

從下面這張圖可以看到，useMemo 雖然在 re-render 階段會優化效能，但只有在 array 長度 > 1000 才有顯著效益，但 useMemo 所帶來的 initial render 的負擔也是非常巨大，array 的長度在 1000 以下，基本上根本不需要 useMemo。

![useMemo mount 效能會更差，在 re-render 時才有改善效能的作用](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3a7da26becc4493caa84b24313cd2573~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp)
*useMemo mount 效能會更差，在 re-render 時才有改善效能的作用* [^useMemo]

況且如果真的需要做這麼大量的計算，也應該要考慮，這些計算在前端做是否合理?

## 小結

經由上面的內容，我們應該要知道，當使用 React.memo、useMemo、useCallback 作為優化的工具時：

- `useMemo` 和 `useCallback` 是拿來**緩存**計算數值、function 的工具，並不是使用了 `useMemo` 或 `useCallback` 就可以保證元件不會 re-render
- 在一個大前提下：**父元件重新渲染**，除了 `React.memo` ，上述兩個 hook ，對避免 re-render **都不管用**。
- `React.memo` 會在 props 改變前提下重新渲染，因此如果有個元件如下，在 `onClick` 未被 `useCallback` 緩存時，即使這個元件使用了 `React.memo` ，也會因為父元件重新渲染時，又重新定義了 `onClick` 這個 function ，而重新渲染。
    
```jsx
const Btn = ({onClick}) => React.memo(<Button onClick={onClick} />);

const Container = () => {
  const [isChange, setIsChange] = useState(false);
  const onClick = () => {
    setIsChange(!isChange);
  };
  return <Btn onClick={onClick} />
;
```
    
- `useMemo` 和 `useCallback` 只有在 re-render 的時候有優化效用，**初始渲染**下，他們不只無用，還可能拖慢 React 的工作，也就是說，若是在不當的使用下，使用這兩個 hook 反而會拖慢效能。
- 使用 `React.memo`、`useMemo`、`useCallback` 都必須謹慎，當你認為你必須使用時，都應該考慮到使用這些方法是在**拿你的記憶體去跟效能做交易**。

# 不需要上述 React 優化函式的優化方案

由前面的敘述可知，state 和 props 變更是觸發 render 的關鍵要素之外，父元件的 re-render 也是造成子元件 re-render 的主要原因，也就是說，透過設計，就算不使用上述 React 提供的優化函式，只要把握不讓 state、props 改變，以及避免父元件 re-render ，也可以達到效能優化的目的。

下面整理出幾種可操作的方式：

## 讓 state 只影響他該影響的範圍

此方法的重點在於，避免 state 影響到其他不相關的元件，比如上面看過的例子：

```jsx
const Content = () => <div>I'm Content!</div>

const App = () => {
  const [state, setState] = useState()
  const onClick = () => setState(!state)

  return (
    <>
      <button onClick={onClick}>change!</button>
      <Content /> {/* Content 沒有 props 但也一樣會 rerender*/}
    </>
  )
}
```

`Content` 沒有 props 但卻會因為 `state` 變化而重新渲染。此時除了使用 `React.memo` ，你還可以將 `button` 拆成新元件 `Btn`，將 `state` 移到 `Btn` 裡面，如此一來，`state` 只會影響到 `button` 的範圍：

```jsx
const Content = () => <div>I'm Content!</div>

const Btn = ({children, ...props}) => {
  const [state, setState] = useState();
  const onClick = () => setState(!state);

  return <button onClick={onClick} {...props}>{children}</button>
};

const App = () => {
  return (
    <>
     <Btn>change!</Btn>
     <Content /> {/* App 不會 re-render，Content 也不會 */}
    </>
  )
};
```

## Children props

前面提到 React 的父層元件 re-render 的同時，底下的所有子元件都會跟著 re-render，React.memo 則是為了阻止 prop 沒有改變的子元件也跟著 re-render 所產生出來的解法。但其實我們也可以利用 children props 來阻斷這個 re-render 的連鎖效應。

作法也很簡單，只要將不想受影響的子元件獨立出來，在祖父元件裡使用 children props 傳到原本的父元件即可：

```jsx
const Child = () => <p>Hello World!</p>

const Parent = ({children}) => {
  const [count, setCount] = useState(0);
  return (
    <>
     <button onClick={() => setCount(count + 1 )}>{count}</button>
       {children}
    </>
  )
}

const GrantParent = () => {
  return (
    <Parent>
      <Child />
    </Parent>
  )
}
```

這個方法不需要 React 記憶 props、比對新舊 props 的差異，只需要調整結構就可以達到避免重渲染的效果。

這個方法也可以優化使用 Context 所影響的子元件 re-render 問題，如果使用 Context 原本的 Code 可能會長這樣[^childrenProps]：

```jsx
import { useState, useContext, createContext } from 'react';

const Context = createContext();

const ChildWithCount = () => {
  const { count, setCount } = useContext(Context);
  console.log('ChildWithCount re-renders');
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <p>Child</p>
    </div>
  );
};

const ExpensiveChild = () => {
  console.log('ExpensiveChild re-renders');
  return <p>Expensive child</p>;
};

const CountContext = () => {
  const [count, setCount] = useState(0);
  const contextValue = { count, setCount };
  return (
    <Context.Provider value={contextValue}>
      <ChildWithCount />
      <ExpensiveChild /> // Imagine re-rendering this component is expensive
    </Context.Provider>
  );
};

const App = () => {
  return <CountContext />;
};

export default App;
```

在這種情況下，CountContext 下面的 ChildWithCount 從 context 接收到 setCount ，修改 count 則會導致 CountContext state 改變而造成 re-render，這邊也可以使用 children props 來改善：

```jsx
import { useState, useContext, createContext } from 'react';

const Context = createContext();

const ChildWithCount = () => {
  const { count, setCount } = useContext(Context);
  console.log('ChildWithCount re-renders');
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <p>Child</p>
    </div>
  );
};

const ExpensiveChild = () => {
  console.log('ExpensiveChild re-renders');
  return <p>Expensive child</p>;
};

const CountContext = ({ children }) => {
  const [count, setCount] = useState(0);
  const contextValue = { count, setCount };
  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};

const App = () => {
  return (
    <CountContext>
      <ChildWithCount />
      <ExpensiveChild />
    </CountContext>
  );
};

export default App;
```

## React Key

有沒有搞錯 React Key 也可以拿來優化效能？的確 React Key 並不是為了優化效能而產生的，但我們可以利用它的特性來達成優化的目的。

首先，我們必須先搞懂 React Key 的工作機制。

### React Key 的運作原理

在官方文檔中，Key 的描述是寫在 [Rendering List](https://react.dev/learn/rendering-lists#rules-of-keys) 這一章節，在開發過程中，使用 array 形式的 children 也通常會被 console 中的警告要求帶入 `unique “key” prop` ，官方說明使用 keys 有兩個原則：

> - **Keys must be unique among siblings.** However, it’s okay to use the same keys for JSX nodes in *different* arrays.
>     *在兄弟元素之間 Key 必須保持獨特。但是如果是不同的 JSX array 使用相同的 Key 是可以的。*
>     
> - **Keys must not change** or that defeats their purpose! Don’t generate them while rendering.
>     *Key 一旦設定就不可以改變，否則會破壞使用 Key 的初衷。請避免在渲染時重新產生 key。*
>     

根據官方文件，使用 Key 的目的是，讓 React 可以從一堆類似的元件中辨識元件，而不是根據他的順序來辨認。React 靠 Key 來辨識元件是否有所更動，進而針對 DOM 進行操作。

前面也提過操作 DOM 是最耗費效能的操作之一。Key 其實是用來幫助 React 優化效能的標誌。

當你沒有設定獨特的 Key 時，React 會自動默認 Array 的 index 當作 key，但 index 會在針對 array 進行排序、插入、刪除時產生變化，React 可能會把分明不相同的元素視為是同一個元素，造成不必要的 re-render，這也是為什麼我們要避免使用 index 當作 Key 的原因之一。

![元件只是換了順序，元件本身其實不需要 re-render，但因為 React 將 index 相同的元件視為同個元件，認為該元件的值 (Australia ⇒ UK, UK ⇒ Australia) 產生了變化，導致全部元件都 re-render](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/984520e1994b494781ebac9803ae6ee9~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
*元件只是換了順序，元件本身其實不需要 re-render，但因為 React 將 index 相同的元件視為同個元件，認為該元件的值 (Australia ⇒ UK, UK ⇒ Australia) 產生了變化，導致全部元件都 re-render* [^ReactKey]

![使用唯一值時，component 都不會 re-render，只是更換順序](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1dc099838dbc4122b5919a5f01a79ca6~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
*使用唯一值時，component 都不會 re-render，只是更換順序* [^ReactKey]

同樣的，隨機產生的 key 也應該避免使用。這會讓 React 無法辨識元件，造成每次 render 都會把原本就的元素刪掉再重新創建元素，即便他們實際上沒有改變。這甚至可能比什麼都不加更糟。

![component 使用 index 做為 key，只會觸發 re-render](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37c2d7234a8e49c78fd3a917802dc660~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
*component 使用 index 做為 key，只會觸發 re-render* [^ReactKey]

![使用隨機值作為 key 將永遠不會觸發 re-render](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24043b27e2cd49d0a8da9b43bc231b82~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
*使用隨機值作為 key 將永遠不會觸發 re-render* [^ReactKey]

總和來說，React re-render 的過程大致上會是這樣：

1. React 生成元素「之前」和「之後」的快照
2. React 識別頁面中已存在的元素，以便重新使用他們，而非重新創建
    1. 如果 `key` 存在，React 會將 「之前」和「之後」元素中，`key` 值相同的元素視為同一個元素
    2. 如果 `key` 不存在，React 會默認 index 為 `key`
3. React 辨識完元素後，會開始進行：
    1. `unmount` ： 刪除 re-render 「之前」存在，但「之後」不存在的元素
    2. `mount` ： 創建「之前」不存在的元素
    3. `re-render`：更新「之前」和「之後」都存在的元素

### React Key 要怎麼拿來優化效能?

前面我們知道 key 是 React 拿來判斷要將元件 re-render 或 mount 的基準，所以我們可以利用這個特性，讓 React 用 re-render 取代元素被卸載又被重建的行為。

上面提到，使用 index 當作 key 有可能造成不必要的 re-render，但假設你的 array 數量固定、排序不變的狀況下，使用 index 作為 key 會比使用  unique 的 key 來的更好，例如分頁列表就是屬於這種形式：

![Momo購物商品頁](https://i.imgur.com/njDqLrF.png)

由於每一頁的資料都固定是 12 筆，如果使用的是 unique key ，當分頁切換時，第一頁的 12 個商品 component 都會被卸載，並且重新創建第二頁的 12 個商品項目。

但如果使用 index 當作 key，第一頁載入所創建的 12 個商品元件不會被卸載，而是會被當作是資料變更，進行將第二頁資料傳入的 re-render 形式，相比卸載又加載，re-render 更加節省效能。

總結一下，使用 react key 的幾個要點：

- **不使用隨機值作為 key** ⇒ 將導致元件永遠都需要重複卸載&加載
- 在列表順序、數量皆保持不變的 **「靜態」列表，使用 index 作為 key** ⇒ 避免複雜元件重複卸載&加載
- 可添加、刪除或修改順序的 **「動態」列表，使用 unique key** ⇒ 可避免不需要的 re-render

# Recap

- **優化 React 效能的大方向有兩個**：
    1. 避免元件 re-render 觸發 diff 演算法，也就是需要避免 state 與 props 的變動
    2. 盡量保持 virtualDOM 的一致，避免 ReactDOM 操作 DOM，也就是需要避免重複加載 & 卸載
- **為了提升效能，React 內建提供的優化方法有**：
    - **React.memo**    
        會記憶元件的 props，在 re-render 時比較新舊 props 是否有異動，有異動才繼續 re-render 元件。
    - **useMemo**
        拿來緩存昂貴的計算使用，只在 dependency 變更的狀況下才重新執行，並不能阻止使用該值的元件 re-render。
    - **useCallback**
        跟 useMemo 類似，拿來緩存在元件內的 callback function，並且只在 dependency 變更的狀況下才重新定義。但一樣不能阻止使用該值的元件 re-render，必須搭配 React.memo 才有效果。
        
    
    👉 以上 3 種方法都需要 React 撥出額外的記憶體去記憶 & 比較 dependency，使用時必須記得：
    
    > **每當使用以上方法，都是在拿你的記憶體跟效能做交易**
    
- **而根據 React 優化的大方向來說，也有不需要使用 React 提供的 function 的方法有**：
    1. 調整 component 結構，避免被不相干的 state 影響元件 re-render
        - 移動 state，讓 state 只存在在需要使用的元件內
        - 活用 children props，將不受 state 影響的元件，以 children props 的方式傳入該元件
        
    2. 不使用隨機值作為 key，並可活用 React key 的特性：
        - 「靜態列表」(數量排序不變，僅內容變化) 使用 **index** key ，可避免重複卸載與加載 (僅 re-render)
        - 「動態列表」(可增加、刪減、調整排序) 使用 **unique** key，可減少不必要的 re-render

# 參考&引用資料

[^childrenProps]:[React components - when do children re-render?](https://whereisthemouse.com/react-components-when-do-children-re-render)

[^ReactQuiz]:[React 性能優化大挑戰：一次理解 Immutable data 跟 shouldComponentUpdate](https://blog.huli.tw/2018/01/15/react-performance-immutable-and-scu/)

[^useMemo]:[【译】你真的应该使用useMemo吗? 让我们一起来看看 - 掘金](https://juejin.cn/post/6969874579028197413?searchId=202311020934016CB13EE5CB8BE9F008DC)

[^useMemo&Cb]:[「好文翻译」为什么你可以删除 90% 的 useMemo 和 useCallback ？ - 掘金](https://juejin.cn/post/7251802404877893689)

[^ReactKey]:[「好文翻译」React key属性：高性能列表的最佳实践 - 掘金](https://juejin.cn/post/7257022428194521145)