本文翻译自React re-renders guide: everything, all at once,文章写的很棒,还有视频讲解,强烈推荐大家品鉴一下~
WHAT:React 中的重渲染是什么?
在谈论 React 性能时,我们需要关心两个主要阶段:
- initial render --- 组件首次出现在屏幕上时发生
- re-render --- 已在屏幕上的组件的第二次及后续渲染
re-render发生在React需要用一些新数据更新应用程序时。通常,这是由于用户与应用程序交互或通过异步请求或某种订阅模型传入的外部数据引起的。
无交互的应用程序,如果没有任何异步数据更新,将永远不会进行re-render ,因此不需要关心re-render性能优化。
什么是必要和非必要re-render
- 必要的re-render :组件是状态更改的起点,或组件使用新的数据导致的重渲染。例如用户在输入框中输入文字,管理其状态的组件需要在每次敲击键盘时更新自己的
value
- 非必要的re-render:由于错误或低效的应用程序架构,导致组件通过不同的重渲染机制在应用程序中传播的重渲染。例如,用户在输入框中输入文字,而整个页面在每次敲击键盘时都会重渲染,那么这个页面就进行了不必要的重渲染。
非必要的重渲染本身不是问题:React 是非常快的,通常能够在用户没有注意到的情况下处理它们。
但是!如果重新渲染过于频繁和/或涉及非常重量级的组件,这可能会导致用户体验出现"卡顿",每次交互都有明显的延迟,甚至应用程序完全无法响应。
WHEN:重渲染发生的时机
导致组件重渲染的原因有四个:
- 状态更新 --- state
- 父组件重新渲染
- 上下文更新 --- context
- hooks发生变化
还有一个很大的误区 :当组件的 props 改变时,就会发生重新渲染。就其本身而言,这是不正确的
状态更新导致的重渲染
当组件的状态发生变化时,它将重新渲染自身。通常,它发生在回调函数或 useEffect
中。
状态更改是所有重新渲染的"根源"
父组件重新渲染导致的重渲染
当一个组件重新渲染时,其所有的子组件都会重新渲染
组件的更新总是沿着组件树向下的 ,即子级的重新渲染不会触发父级的重新渲染(存在一些特殊情况,感兴趣的伙伴可以看一下这篇文章。)
Context更新导致的重渲染
当 Context Provider
中的值发生更改时,所有使用此 Context 的组件都将重新渲染,即使它们不直接使用数据的更改部分。
这些重新渲染不能直接用记忆化来阻止,但有一些变通技巧可以解决。
Hooks变化导致的重渲染
发生在Hook内部的一切都"属于"使用它的组件 。关于Context和State变化的相同规则也适用于这里---即hook内部state和context的修改会导致组件的重渲染
误区:props更改导致重渲染
当谈论未记忆化的组件的重新渲染时,组件的 props 是否改变并不重要。
因为props是在父组件中更新的。这意味着父组件必须重新渲染,这将触发子组件的重新渲染,本质上是父组件的重渲染导致子组件的重渲染,与 props 无关。
仅当使用记忆技术( React.memo
、 useMemo
)时,props 的更改才变得重要。
How:通过组合阻止重渲染
⛔反模式:在渲染函数中创建组件
在另一个组件的渲染函数内创建组件是一种反模式,可能是最大的性能杀手。每次重新渲染时,React 都会重新安装该组件(即销毁它并从头开始重新创建它),这比正常的重新渲染要慢得多。最重要的是,这将导致以下错误:
- 重新渲染期间可能出现内容"闪烁"
- 每次重新渲染时组件中的状态都会重置
- 每次重新渲染时都不会触发 useEffect 的依赖关系
- 如果某个组件获得焦点,焦点将会丢失
永远不要在组件中创建另一个组件
✅向下移动状态
这种模式通常是组件十分的复杂,其某个状态只在很小的一部分中使用,我们可以将其抽离成单独的组件,在这个抽离出来的组件中管理这个状态。一个典型的例子是在一个复杂的组件中,通过按钮点击打开/关闭一个对话框,而这个组件渲染了页面的很大一部分。
在这种情况下,控制模态对话框外观、对话框本身和触发更新的按钮的状态,可以封装在一个更小的组件中。这样,较大的组件就不会在这些状态变化时重新渲染。
✅children 作为 props
这也可以称为"将状态包裹在子组件中"。这种模式类似于"向下移动状态":将状态变化封装在一个较小的组件中 。这里的区别在于,状态用于渲染树中缓慢部分的元素,因此无法轻松提取它。一个典型的例子是附加到组件的根元素上onScroll或onMouseMove回调函数。
在这种情况下,状态管理和使用该状态的组件可以提取到较小的组件中,并且慢速组件可以作为 children
传递给它。从较小的组件角度来看 children
只是 prop,因此它们不会受到状态变化的影响,因此不会重新渲染。
✅组件作为 props
与children
作为 props几乎相同,具有相同的行为:它将状态封装在较小的组件内,并且重型组件作为 props 传递给该组件。props 不受状态变化的影响,因此重型组件不会重新渲染。
How:使用 React.memo 防止重新渲染
将组件包裹在 React.memo 中将停止由渲染树上游触发的一系列重渲染,除非这个组件的 props 发生了变化。
✅带有 props 的组件
所有非基本类型的 props 都必须被 memo 起来,React.memo 才能工作
✅组件作为 props 或 children
React.memo
必须应用于作为children
/ props
传递的元素
记忆化父组件是行不通的:children
和props
是对象(引用类型),因此它们会随着每次重新渲染而改变。
How:使用 useMemo/useCallback 提高重渲染性能
⛔反模式:无作用的 useMemo/useCallback
单独记忆化 props 不会阻止子组件的重新渲染。如果父组件重新渲染,无论子组件的 props 如何,它都会触发子组件的重新渲染。
✅必要的 useMemo/useCallback
如果子组件包装在 React.memo
中,则必须记忆化所有非基本类型的 props
如果组件使用非基本类型作为useEffect
、 useMemo
、 useCallback
等 hooks 中的依赖项,则应将其记忆化
✅useMemo 用于昂贵的计算
useMemo
的用例之一是避免每次重渲染时进行昂贵的计算。
useMemo
有其成本(消耗一些内存并使初始渲染稍慢),因此不应将其用于每个计算。在 React 中,挂载和更新组件通常会是大多数情况下最昂贵的计算。
因此,useMemo
的典型用例是记忆化 React 元素。通常是现有渲染树的部分或生成的渲染树的结果,比如返回新元素的 map
函数。 与组件更新相比,纯 JavaScript 操作(如排序或过滤数组)的成本通常可以忽略不计。"
How:提高列表的重渲染性能
除了常规的重渲染规则和模式之外, key
属性还会影响 React 中列表的性能。
重要提示 :仅提供 key
属性并不会提升列表的性能。为了防止列表元素重渲染,你需要将它们包裹在 React.memo 中,并遵循其所有最佳实践。
key
的值应该是一个字符串,该字符串在列表中每个元素的重新渲染之间保持一致。通常是每项的 id
或数组的 index
作为 key
。
如果列表是静态的 ,即不添加/删除/插入/重新排序元素,则可以使用数组的 index
作为 key
。
在动态列表上使用数组的索引可能会导致:
- 如果列表项有状态或任何非受控制元素(如表单输入),可能会出现bug
- 如果列表项被包裹在 React.memo 中,则性能可能会下降
⛔反模式:使用随机值作为列表中的 key
随机生成的值绝不应该用作列表中 key
。它们将导致 React 在每次重新渲染时重新安装每一项,这将导致:
- 列表的性能非常差
- 如果列表项有状态或任何非受控制元素(如表单输入),可能会出现bug
How:阻止Context引起的重渲染
✅记忆化 Provider 的 value
如果 Context Provider 不是放在应用程序的最根部,且由于其祖先的变化可能会导致自身重渲染,那么它的 value 应该被记忆化。
✅拆分数据和 API
如果在 Context 中存在数据和 API(getter 和 setter)的组合,则它们可以拆分为同一组件下的不同 Provider。这样,仅使用 API 的组件在数据更改时不会重新渲染。
✅将数据分割成块
如果Context管理一些独立的数据块,它们可以在同一个 Provider 下被拆分成更小的 Provider。这样,只有变更数据块的消费者会重新渲染。
✅Context 选择器
即使使用 useMemo
钩子,也无法阻止使用部分 Context 值的组件重新渲染,就算所使用的数据片段没有更改
通过使用高阶组件和 React.memo
实现的Context选择器可以解决这个问题