理解如何使用 React memo 和 useMemo - 優化組件

React.memo

reference

1
2
3
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});

React.memo 是一個 higher order function

如果你的 function component 每次得到相同 prop 的時候都會 render 相同結果,你可以將其包在 React.memo 之中,透過快取 render 結果來在某些情況下加速。這表示 React 會跳過 render 這個 component,並直接重用上次的 render 結果。

React.memo 只會確認 props 的改變。如果你的 function component 被包在 React.memo 內,實作中具有一個 useState、useReducer 或 useContext Hook,當 state 或 context 改變時,它仍然會持續 rerender。

React.memo 可以用來優化效能,但不應該依賴它來 阻止 組件渲染,這可能會產生無法預期的錯誤。

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app.js
import { useState } from "react";
import "./styles.css";

export default function App() {
const [inputText, setInputText] = useState("");

console.count("App.js");

return (
<div className="App">
<header className="App-header">
<h1>React.memo demo</h1>
Input text: {inputText} <br />
<input type="text" onChange={(e) => setInputText(e.target.value)} />
</header>
</div>
);
}

setInputText執行後,組件會重新渲染而 console.log('App.js') 會被觸發。


接下來改寫讓範例多一個子組件,像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState } from "react";
import "./styles.css";

import Title from "./Title";

export default function App() {
const [inputText, setInputText] = useState("");

return (
<div className="App">
<header className="App-header">
<Title text="React.memo demo" />
Input text: {inputText} <br />
<input type="text" onChange={(e) => setInputText(e.target.value)} />
</header>
</div>
);
}
1
2
3
4
5
6
7
8
// title.js
const Title = ({ text }) => {
console.log("Title.js");

return <h1>{text}</h1>;
};

export default Title;

身為父組件的 <App /> 裡的 setInputText 執行後,子組件 <Title /> 裡的 console.count('Title.js')也會被執行,這是因為 app.js 裡的 state 在每一次被改變後都會讓組件重新渲染。
假想今天一堆不太相干的子組件持續被重新渲染,這可能導致嚴重的效能問題。


接下來範例裡的子組件用 React.memo 包起來

1
2
3
4
5
6
7
8
9
10
//title.js
import { memo } from "react";

const Title = ({ text }) => {
console.log("Title.js");

return <h1>{text}</h1>;
};

export default memo(Title);

會發現現在當父組件的 <App /> 裡的 setInputText 執行並重新渲染時,子組件 <Title /> 已經不會跟著重新渲染了。這是因為子組件現在是透過快取 render 結果,而且只會根據 props 或是內部的狀態來決定要不要重新渲染組件。


React.useMemo

reference

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 回傳一個 memoized 的值。

傳遞一個「建立」function 及依賴 array。useMemo 只會在依賴改變時才重新計算 memoized 的值。這個最佳化可以避免在每次 render 都進行昂貴的計算。

要謹記傳到 useMemo 的 function 會在 render 期間執行。不要做一些通常不會在 render 期間做的事情。例如,處理 side effect 屬於 useEffect,而不是 useMemo。

如果沒有提供 array,每次 render 時都會計算新的值。


example

接下來在剛剛範例加一些功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app.js
import { useState } from "react";
import "./styles.css";

import Title from "./Title";

export default function App() {
const [inputText, setInputText] = useState("");
const [counter, setCounter] = useState(0);

const getReversedText = () => {
console.count("getReversedText");
return inputText.split("").reverse().join("");
};

return (
<div className="App">
<header className="App-header">
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
<Title text="React.memo demo" />
Input text: {inputText} <br />
Reversed test: {getReversedText()}
<input type="text" onChange={(e) => setInputText(e.target.value)} />
</header>
</div>
);
}
  1. 新增一個 button,每次按下 counter + 1
  2. 新增一個 getReversedText,每次會 reutrn 將 inputText 倒轉後的新字串

當按下 button 後組件會重新渲染,會發現跟 counter 不相干的 getReversedText 也一起重新渲染了。

接下來使用 useMemo 改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useMemo } from "react";
import "./styles.css";

import Title from "./Title";

export default function App() {
const [inputText, setInputText] = useState("");
const [counter, setCounter] = useState(0);

const reversedText = useMemo(() => {
console.count("getReversedText");

return inputText.split("").reverse().join("");
}, [inputText]);

return (
<div className="App">
<header className="App-header">
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
<Title text="React.memo demo" />
Input text: {inputText} <br />
Reversed test: {reversedText}
<input type="text" onChange={(e) => setInputText(e.target.value)} />
</header>
</div>
);
}

reversedText 用 useMemo 包起來,並監聽 inputText,當 inputText 改變才重新渲染。

  1. 可以發現現在當按下 buttonreversedText 裡面的 console.count() 已經不會執行,當 inputText 改變時才會重新計算 reversedText 的回傳值。

final

React.memoReact.useMemo 可以用來優化組件,我們已經通過一些例子來說明並試著解決效能問題。

要再次記得不要將 React.memoReact.useMemo 混淆:

  • React.memo 用來包裝組件並防止重新渲染。
  • React.useMemo 用來回傳一個 memoized 的值。

請記住,大多數的 React 優化行為都為之尚早,在大部分的默認情況下,React 的速度是很快的,因此每項優化行為都是可選的,這是為了防止某些行為開始變得緩慢。