當我們設計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
2
3
4
5
6
7
8
9
10
11
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" />
</div>
);
};

export default App;
1
2
3
4
5
6
7
8
9
10
11
const Card = ({ title }) => {
console.log('has rendered');
return (
<div>
<h1>{title}</h1>
<p>This is content.</p>
</div>
);
};

export default Card;

當click事件被執行後,App會進行re-render的動作,所以Card也會進行render的動作。
可是事實上Card的props並沒有改變,裡面的結構也沒有改變的必要。
因此我們可以針對Card進行render的優化:

1
2
3
4
5
6
7
8
9
10
11
const Card = ({ title }) => {
console.log('has rendered');
return (
<div>
<h1>{title}</h1>
<p>This is content.</p>
</div>
);
};

export default React.memo(Card);

使用HOC很簡單,只要簡單的包起來就能擁有特性。
包好之後,你會發現點擊App上的button,Card並不會被re-render了。

但是…如果今天App是長這樣呢?

1
2
3
4
5
6
7
8
9
10
11
12
const App = () => {
const [count, setCount] = useState(0);
const tags = ['happy', 'sad', 'madness'];
return (
<div>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" tags={tags} />
</div>
);
};

export default App;

你會發現你觸發Click事件之後,Card竟然開始re-render了!!
你可能心想:傳過去的明明就是const呀!為什麼會re-render?
因為memo只會進行shallow compare,再加上請回想JS的特性
再加上tags他本身是一個物件,每次產生都會分配到不同的記憶體位置,導致memo認為兩者是不相同的。

方法一

不過memo提供第二個參數使用,可以傳送一個compare function進行你要的運算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps) === JSON.stringify(nextProps);
}

const Card = ({ title }) => {
console.log('has rendered');
return (
<div>
<h1>{title}</h1>
<p>This is content.</p>
</div>
);
};

export default React.memo(Card, areEqual);

Demo

See the Pen React.memo using compare function Example by YangYang (@yyisyou) on CodePen.

方法二

其實只要把不會更動的const放到component外面即可,這樣App re-render的時候,才不會把tags重新分配一個新的記憶體位置。

1
2
3
4
5
6
7
8
9
10
11
12
const tags = ['happy', 'sad', 'madness'];
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" tags={tags} />
</div>
);
};

export default App;

Demo

See the Pen React.memo Example by YangYang (@yyisyou) on CodePen.

useMemo

useMemo是React hooks提供的一個更便捷的一種memo方式,它可以監聽特定的資料是否改變,再去改變指定的資料,如此一來可以達到和React.memo想要達成的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const App = () => {
const [count, setCount] = useState(0);
const tags = useMemo(() => {
return ['happy', 'sad', 'madness'];
}, []);
return (
<div>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" tags={tags} />
</div>
);
};

export default App;

第一個參數放置的一個回傳需要執行的function
第二個參數是,當特定的值改變的話,將會執行useMemo的動作,以這個例子因為沒有相依性,所以可以填寫空陣列,表示只會在一開始執行一次。

Demo

See the Pen useMemo Example by YangYang (@yyisyou) on CodePen.

useCallback

今天如果突然新增一個function用來回傳一個count文案的contentHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App = () => {
const [count, setCount] = useState(0);
const [trigger, setTrigger] = useState(false);
const tags = useMemo(() => {
return ['happy', 'sad', 'madness'];
}, []);
const contentHandler = () => `I did ${count} times in my current job.`;
return (
<div>
<button type="button" onClick={() => setTrigger(!trigger)}>other tigger</button>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" tags={tags} contentHandler={contentHandler} />
</div>
);
};

export default App;

並且在Card裡面使用useEffect,右邊的相依陣列放空的表示只會在初次render的時候執行一次。
但是你會發現這樣做就會開始發生re-render了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Card = ({ title, contentHandler }) => {
console.log('has rendered');
useEffect(() => {
const content = contentHandler();
console.log(content);
}, [contentHandler]);
return (
<div>
<h1>{title}</h1>
<p>This is content.</p>
</div>
);
};
export default Card;

為了區別container的改變和外部事件的改變,因此新增了trigger button。
你會發現當點選了trigger,一樣會發生re-render的狀況。
原因也是和最上面我們在探討tags物件重新產生一樣的道理,由於anonymous function在container中不斷被產生與傳值,因此React.memo在執行shallow compare的時候,會認為與前一次的function是不一樣的。

以一開始的例子,我們用useMemo把array包起來解決了,那這個例子換成function的情況,可以使用useCallback!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App = () => {
const [count, setCount] = useState(0);
const [trigger, setTrigger] = useState(false);
const tags = useMemo(() => {
return ['happy', 'sad', 'madness'];
}, []);
const contentHandler = useCallback(() => `I did ${count} times in my current job.`, [count]);
return (
<div>
<button type="button" onClick={() => setTrigger(!trigger)}>other tigger</button>
<button type="button" onClick={() => setCount(count+1)}>add</button>
<Card title="Happy Birthday" tags={tags} contentHandler={contentHandler} />
</div>
);
};

export default 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
2
3
4
5
6
7
8
9
const tags = useMemo(() => {
return ['happy', 'sad', 'madness'];
}, []);
console.log('This is what useMemo looks like: ', tags);

const contentHandler = useCallback(() => {
return `I did ${count} times in my current job.`;
}, [count]);
console.log('This is what useCallback looks like: ', contentHandler);

區分兩者最大的不同在於

  • 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