iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
Modern Web

React 從 0.5 到 1系列 第 8

[鐵人賽 Day08] 如何使用 memoization 方法減少 useContext 非必要 re-render 的效能問題?

前情提要

在看 context API 相關的文件時,發現了一篇 React repo 中的討論,主題是 useContext 如何避免非必要的重複 render,React team 提供的解答會使用 memo()、useMemo() 等方法。因為 memo() 在我的記憶裡,只會追蹤 props 的改變,所以更加好奇到底可以怎麼實作?

以下是我的閱讀筆記,並且會補充、簡介 React 中 memoization 的 API,希望可以幫助到各位。討論原文可以在這裏 https://github.com/facebook/react/issues/15156 看到。

Memoization 的方法很多

React 有很多 memoization 的方法可以使用,class component 可以使用 shouldComponentUpdate, React.memo() 則是可以使用在 function component 裡頭,此外,還有一支 Hook API 叫做 useMemo()。

React.memo 跟 useMemo 又有什麼區別?React.memo() 是一個 HOC,可以用來包住元件,根據 props 的變更決定 render 與否;而 useMemo 是用來在一個元件中、包住函示的,我們可以使用它來確保 function 內的 value 只有在 dependencies 變動時,才會被重新運算(re-computed)。

仔細介紹文章中的 React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 這裡寫你的元件 */
});

當你的元件一直接收到相同的 props 因而不斷 render 出同樣的結果時,為了提升效能、減少不必要的 render,你可以使用 React.memo 把你的元件包在裡面,如果 props 沒有改變, React 會略過 re-render 你的元件,並且複用前一次 render 的結果。

但如果你的 component 含有 useState、useReducer、useContext 這類的 hook,當 state 或者 context 改變的時候,元件依然會被 re-render,因為 memo 只比較 props

另外,memo 只會 shallow 的比較你的 props 變更,你也可以自製這個比較差異的函示,把它當成第二個參數傳入即可。

function MyComponent(props) {
  /* 元件內容 */
}
function areEqual(prevProps, nextProps) {
  /*
  用來比較差異的函示
  */
}
export default React.memo(MyComponent, areEqual);
/// 注意這裡的 areEqual 是 return true or false

讓我們看一個情境

一位工程師同仁發布了一個 report,以下是他在嘗試比對 context 以節省效能的過程中遇到的疑問:

// “我應該在 memo() 的第二個參數 callback 裡使用 context & nextProps 做差異比較,
// 還是有其他提升 useContext 效能的做法?”
React.memo(() => {
let globalState = useContext(AppContext);
	// render something
}, (prevProps, nextProps) => {
		// do comparison
});

你會怎麼做呢?討論中提到了三個作法:

最理想的做法是:假設被用在元件裡的 value 是 globalState.theme,而 globalState.theme 也被許多元件所需要,但 globalState 的變更太頻繁,會導致有使用 globalState.theme 也一起頻繁 render,此時我們可以把 globalState.theme 拆出來,變成一支獨立的 context,這是變更最小的方法。

function Button() {
  // 這裏把 theme 拆成獨立的 ThemeContext
  let theme = useContext(ThemeContext);
  return <ExpensiveTree className={theme} />;
}

第二種方法是把元件拆分成兩個,在中間放上 memo():

function Button() {
  let globalState = useContext(AppContext);
	// 傳送 globalState.theme 這個更特定的 context 到下層,降低 re-render 機會
  let theme = globalState.theme;
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
	// 其餘的 render 邏輯放在這
	// 這一層會被 memo 住,藉此節省效能
  return <ClickArea color={theme} />;
});

如果你無法使用第一種方法把 context 拆開,你可以試試看把元件拆開,然後傳送更特定的 props 給最下層的那個元件(<ThemedButton>)。你的外層元件(<Button>)還是會容易被 re-render,但下層元件就可以被 keep 住,這樣一來,應該可以挽救很多效能(因為外面那一層也沒有做什麼事情)。

第三種方法是使用 useMemo:

最後一種方法,是把 return component 包在 useMemo 裡,只要 useMemo dependencies 不改變 React 就不會 re-render,跟第二種方法相似。

function Button() {
  let globalState = useContext(AppContext);
  let theme = globalState.theme; 

  return useMemo(() => {
   // 其餘的 render 邏輯放在這
	 // 這一層會被 memo 住,藉此節省效能
    return <ThemedButton theme={theme} />;
  }, [theme])
}

最後,底下的討論也提到,如果你的 APP 內部結構複雜龐大、要共用的資訊多,那可以考慮改用 Redux 來管理狀態。


上一篇
[鐵人賽 Day07] 為何不該使用 index 當作 Key 值 ?——React render 更新機制解釋
下一篇
[鐵人賽 Day09] React Context(上)-單純的用法
系列文
React 從 0.5 到 115
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言