[铁人赛 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 来管理状态。


<<:  Day 09. Zabbix 监控 ESXi vSphere

>>:  [Day - 08] - Spring 通用注入各式元件运作与设计

[Flutter ] Free fake API 为资料库,以 FutureBuilder + http 抓取

程序版本: Flutter 2.2.1 使用JSONPlaceholder为 Free fake A...

Day20 让电脑透过数据机和有线、无线网路传递讯息

上一回讲的是透过数据机连结各种电脑周边 今天来分享数据机更强大的功能,传递讯息 可以先查询 mode...

Day1. 请Matter.js同学自我介绍一下

嗨,虽然说第一篇的目的是简单了解一下Matter.js,但在那之前我要偷渡一下自我介绍XD 这是笔者...

Day 27 - axios

利用 XMLHttpRequest 原理 可用 Node.js 後端进行请求 语法简短 直觉化 r...

Day 3 - HTAP

上一篇提到了TiDB的特色之一,便是实践了HTAP。那HTAP又是什麽东西? HTAP全名Hybri...