【译】React 中的 re-render
2022年8月29日 · 预计阅读时间: 13 分钟
介绍 React 中的 re-render 的概念以及如何防止 re-render 的方法。
原文:https://www.developerway.com/posts/react-re-renders-guide
什么是 React 中的 re-render?
当我们谈论 React 的性能的时候,有两个主要的阶段需要我们关注:
- 初始渲染 - 发生在组件第一次出现在屏幕上时
- 重新渲染 (re-render) - 已经在屏幕上的组件的第二次和任何连续渲染
re-render 发生在 React 需要用新的数据去更新 app 的时候。通常是用户操作、异步请求返回数据或者是订阅数据发生变化产生的结果。
没有任何异步数据更新的不可交互的应用,永远不会 re-render,因此不需要关心 re-render 性能优化。
🧐 什么是必要的和不必要的 re-render?
必要的 re-render - 对发生变化的组件或直接使用新信息的组件进行 re-render。例如,用户输入的时候,input 组件和使用了该 state 的组件需要在每次输入时更新,即 re-render。
不必要的 re-render - 对不依赖变化的 state 的组件进行 re-render。例如,用户输入的时候,整个页面都 re-render 了,那么这就是不必要的 re-render。
对于组件自身不必要的 re-render 不是问题:React 的速度非常快,通常能够在用户没有注意到的情况下处理它们。
然而,如果 re-render 发生得太频繁且发生在非常“重”的组件中,这可能会导致交互都出现延迟,甚至应用程序变得完全没有响应。
什么时候 React 组件会 re-render?
这有四个原因为什么组件会 re-render:state 改变、父(或子)组件 re-render、context 改变和 hooks 改变。
还有一个反直觉结论:当 props 改变时,组件会 re-render,就其自己而言,这不完全正确,下面会进行解释。
🧐 Re-renders 原因:state 改变
当一个组件的 state 改变时,它会 re-render 它自己。通常发生在 callback 或 useEffect
中。
state 改变是所有 re-render 的根因。
🧐 Re-renders 原因:父组件 re-render
组件会 re-render 如果它的父组件发生了 re-render。换个角度说,如果一个组件发生了 re-render,它所有的子组件都会 re-render
子组件的 re-render 不会触发父组件的 re-render(这里有一些注意事项和一些边界情况,详情查看 The mystery of React Element, children, parents and re-renders)
🧐 Re-renders 原因:context 改变
当 Context Provider 中的值发生变化时,所有使用了这个 context 的组件都会 re-render,即使他们没有直接使用变化的数据。这些组件的 re-render 不能直接通过缓存阻止,但是有一些解决方法可以。(查看)
🧐 Re-renders 原因:hooks 改变
在 hooks 里发生的一切都 "属于 "使用它的组件。context 和 state 变化的 re-render 规则在这同样适用:
- hook 里面的 state change 会触发 re-render。
- 如果 hooks 中使用了 context 并且 context 改变了,也会触发 re-render。
hooks 可以链式调用。每个在调用链中的独立 hook 依旧属于调用 hook 的组件,适用上面的规则。
⛔️ Re-renders 原因:props changes (惊人的事实)
在谈论没有缓存的组件的 re-render 时,组件的 props 是否改变并不重要。
props 的改变依托于父组件的更新。这意味着父组件会发生 re-render,所以不管 props 是否改变,子组件都会 re-render。
只有当使用缓存时(React.memo
, useMemo
),props 的改变才变得重要。
通过组合防止 re-render
⛔️ 错误的做法:在 render 函数中创建组件
在另一个组件的渲染函数内创建组件是一种错误的做法,非常影响性能。每次 re-render React 都会 re-mount 这个组件(即先销毁再重新创建),这将比正常的 re-rerender 慢得多。除此之外,这还会导致诸如以下的错误:
- re-render 过程中可能会出现内容的闪现。
- 每次 re-render 组件的 state 都会被重置
- 没有依赖项的 useEffect 每次 re-render 时都会被触发
- 组件会丢失 focus 状态
另外的一些资料:How to write performant React code: rules, patterns, do's and don'ts
✅ 通过组合防止 re-render:状态下移
当一个复杂组件里的某个 state,且这个 state 只用于一小部分时,这种做法是有利的。一个典型的例子是一个复杂组件中通过一个 button 控制 diglog 组件的开/关状态。
在这种情况下,控制 diglog 显隐的状态、diglog 本身和触发更新的按钮可以被封装在一个较小的组件中。这样,大组件不会在这些状态变化时 re-render。
另外的一些资料:The mystery of React Element, children, parents and re-renders, How to write performant React code: rules, patterns, do's and don'ts
✅ 通过组合防止 re-render:children 作为 props
这种做法和“状态下移”类似:它将状态变化封装在一个较小的组件中。这里的不同之处在于,状态用于渲染得慢的元素,不太容易提取它。一个典型的例子是连接到一个组件的根元素的 onScroll 或 onMouseMove 回调。
在这种情况下,可以将状态管理和使用该状态的组件提取到更小的组件中,并且“慢”组件可以作为子组件传递给它。从小组件的角度来看,children
只是 props,不会被 state 改变影响,因此不会 re-render。
另外的一些资料:The mystery of React Element, children, parents and re-renders
✅ 通过组合防止 re-render:组件作为 props
与之前的做法差不多:将状态封装在一个较小的组件中,而重组件作为 props 传递给它。props 不受 state 变化的影响,因此重组件不会 re-render。
当一些重组件独立于状态,但不能作为一个children
提取时,它可能很有用。
在此阅读更多关于将组件作为 props 传递的信息: React component as prop: the right way™️
通过 React.memo 防止 re-render
用 React.memo
包裹组件会阻止因为父组件 re-render 导致的子组件 re-render,除非 props 有变化。
这在渲染不依赖于重新渲染源(即状态、更改的数据)的重组件时很有用。
✅ React.memo: 需要 props 的组件
所有不是基本类型的值都需要缓存,来让 React.memo 可以正常工作。
✅ React.memo: 组件作为 props 或 children
使用 React.memo
的元素必须作为 props 或 children。 缓存父组件将不起作用:children 和 props 是对象,因此它们会随着每次 re-render 改变。
使用 useMemo/useCallback 提升 re-render 性能
⛔️ 错误的做法:不必要的对 props 使用 useMemo/useCallback
在父组件内缓存 props 并不能阻止子组件的 re-render。如果父组件 re-render,不管 props 是否改变,子组件都会被重新渲染。
✅ 必要的 useMemo/useCallback
如果子组件使用了 React.memo
包裹,所有不是基本类型的 props 都应该被缓存。
如果一个组件在 useEffect
、useMemo
、useCallback
,这些 hooks 的依赖项中添加了非原始值的依赖,那么这些依赖需要被缓存。
✅ 对于昂贵计算使用 useMemo
useMemo
的使用情况之一是避免每次 re-render 时进行昂贵的计算。
使用 useMemo
有它自己的一些成本(消耗一些内存并使初始渲染稍慢),所以它不应该被用于每次计算。在 React 中,在大多数情况下,挂载和更新组件将是最昂贵的计算。(除非你在做一些不应该在前端做的事,比如计算素数)
因此,useMemo 的典型用例是缓存 React 元素。与组件更新相比,“纯” javascript 操作(如排序或过滤数组)的成本通常可以忽略不计。
提升 list 的 re-render 性能
除了常规的 re-render,key 属性会影响 React 中列表的性能。
重要:仅提供 key
属性不会提高列表的性能。为了防止 re-render 列表,你需要将它们包装在 React.memo
中并遵循其所有最佳实践。
key
值应该是一个不变且唯一的 string,通常是列表元素的 id
或者 index
。
如果列表是静态的,使用 index
作为 key 是可以的。即列表元素不会增删,插入和排序。
在动态列表中使用 index 作为 key 会导致:
- 如果元素具有状态或任何不受控制的元素(如表单输入),则会出现错误
- 如果元素包装在 React.memo 中,性能会下降
更多资料:React key attribute: best practices for performant lists.
防止由于 context 的 re-render
✅ 防止由于 context 的 re-render: 缓存 Provider value
如果 Context Provider 不是放在 app 的根节点,并且由于其祖先的更改,它可能会重新渲染自身,则应该缓存它的值。
✅ 防止由于 context 的 re-render: 分离数据和 API
如果在 context 中把数据和 API(getters 和 setters)放在一起,则它们可以拆分成不同的 Provider。这样,使用 API 的组件仅在数据更改时不会 re-render。
更多资料:How to write performant React apps with Context
✅ 防止由于 context 的 re-render: 数据分块
如果 Context 管理一些独立的数据块,它们可以被拆分为同一个 Provider 下的更小的 Providers。这样,只有更改块的 consumers 才会 re-render。
✅ 防止由于 context 的 re-render:Context selectors
没有办法阻止使用部分 Context 值的组件重新渲染,即使使用的数据没有更改,即使使用 useMemo
也是如此。
然而可以使用高阶组件和 React.memo
来伪造 Context selector。