本指南解释了什么是re-render,什么是必要re-render和非必要re-render,以及什么会触发React组件的re-render
什么是React中的re-render?
当谈到React性能时,我们需要关心两个主要阶段:
- 初始render - 发生在组件首次出现在屏幕上时
- re-render - 已经出现在屏幕上的组件第二次以及之后持续的render
re-render发生在React需要用新数据更新app时。一般是因为用户与app交互或有一些额外的数据来自一个异步请求或订阅模式。
那些没有一部数据要更新的非交互式app是不会re-render的,所以不需要关心re-render性能优化。
什么是必要re-render和非必要re-render?
必要re-render:是变化源头的组件的re-render,或直接使用新数据的组件。例如,如果用户在输入框打字,那么管理它的状态的组件就需要在每次敲键盘时更新自己,也就是re-render。
非必要re-render:由于一些错误或低效的app架构,通过不同的re-render机制穿过整个app被传递到一个组件引发re-render。例如,如果用户在输入框打字,整个页面随着每次输入re-render,那么这个页面就是不必要re-render。
不必要re-render本身不是什么问题:React非常迅速,它通常能在用户察觉不到的情况下处理他们。
然而,如果re-render太频繁或发生在复杂组件上,这会让用户体验出现"时差",每次交互有明显的延迟,甚至整个app彻底无响应。
React组件自身何时会re-render?
有四个原因会使组件自身re-render:state改变,父组件(或子组件)re-render,context改变,和hooks改变。另外有一大谬误:组建的props改变会造成re-render。就它本身来说,这是不对的(解释如下)。
re-render的原因:state改变
当一个组件state改变,它自身会re-render。这通常发生在回调函数或useEffect hook里。
state改变是所有re-render的根源。
re-render原因:父组件re-render
一个组件的父组件re-render时它自身也会re-render。或者我们反过来说,当一个组件re-render,它也会re-render所有的子组件。
它总是沿着树向下:子组件re-render不会触发父组件re-render(这里有一些警告和边界情况,查看完整指引以获取更多细节www.developerway.com/posts/react...)
re-render原因:context改变
当context provider中的值变化时,所有使用此context的组件都会re-render,即使它们没直接使用数据中变化的部分。这些re-render没法直接通过缓存避免,但存在一些应变方法来模拟它。(见www.developerway.com/posts/react...)
re-render原因:hooks改变
hook里发生的一切都"属于"用到它的组件。关于context和state改变的相同规则也适用:
- hook里state改变会触发"主组件"不可避免的re-render
- 如果hook用到了context并且context值改变,它将触发"主组件"不可避免的re-render
hooks可以是链式的。hook链里每一个单独的hook仍属于"主组件",这对其他任何hook也适用。
re-render原因:props改变(一大谬误)
对于没有缓存过的(not memoized)组件来说,组件的props变化与否并不重要。
为了让props变化,这些props需要被父组件更新。这意味着,父组件不得不re-render来触发子组件re-render而不管它的props如何。
只有使用了缓存技术(React.memo, useMemo),props变化才变得重要。
用组合防止re-render
❌反模式:在render函数里创建组件
在另一个组件的render函数里创建组件是一种堪称最大性能杀手的反模式。在每次re-render时React都将re-mount这个组件(即销毁然后重新从头创建它),这会比正常的re-render慢得多。除此之外,它会导致以下bugs:
- re-render期间内容会"闪烁"
- 随着每次re-render组件里的state会被重置
- 每次re-render会触发没有依赖项的useEffect
- 若一个组件被聚焦,焦点将会丢失
可查看其他资源:www.developerway.com/posts/how-t...
✔ 利用组合防止re-render:state下移
当一个复杂组件管理着状态,并且这个状态只在render树上单独的一小部分上被用到时,这种模式是有益的。典型的例子就是在一个渲染了一大部分页面的复杂组件上点击一个按钮来打开/关闭一个弹窗。
这种情况下,控制modal弹窗出现的的state,弹窗本身,以及触发更新的按钮应该被封装在一个较小的组件里。这样的话,较大的组件就不会随着那些state变化而re-render了。
额外资源:www.developerway.com/posts/react...
✔ 利用组合防止re-render:子组件作为props
也被称作"用state包裹子组件"。这种模式类似于"state下移":它把状态封装在了更小的组件里。不同的是这里的state被用在了一个元素上,这个元素包含render树中更缓慢的部分,所以它不会轻易地被引出。典型的例子是在一个组件的根元素上绑定onScroll或onMouseMove回调函数。
这种情况下,状态管理和使用这些状态的组件被引入到了更小的组件里,这个缓慢的组件可以作为子组件传递给它。对较小的组件来说子组件只是一个prop,所以它们不会被状态的改变影响,因为不会re-render。
更多资源:www.developerway.com/posts/react...
✔ 利用组合防止re-render:组件作为props
与上一个模式非常相似,有着用样的操作:它把状态封装在了更小的组件里,复杂组件被当作props传递。props不会被状态变化影响,所以复杂组件不会re-render。
当一些复杂组件独立于state时很有用,但是不用作为一组被当作子组件引入。
把组件当作props转递的更多信息:www.developerway.com/posts/react...
用React.memo防止re-render
用React.memo包裹组件,当render树的上游某处被触发时会阻止下游render链的改变,即使组件props已经改变。
当渲染一个不依赖于re-render源(即state,变化的数据)的复杂组件时很有用。
✔ React.memo: 带有props的组件
非原始值的所有props都要被React.memo缓存
✔ React.memo: 组件作为props或子组件
React.memo必须作用于被当作子组件/props传递的元素。缓存父组件没有用:子组件和props可能会是对象,所以每次re-render都会改变。
缓存如何作用于父子关系的更多细节可查看这里:www.developerway.com/posts/react...
使用useMemo/useCallback提升re-render性能
❌反模式:props上不必要的useMemo/useCallback
缓存props不会阻止子组件的re-render。如果父组件re-render,不管子组件的props如何,都会触发子组件re-render。
✔ 必要的useMemo/useCallback
如果子组件被包裹在React.memo里,所有非原始值的props都必须被缓存。
如果一个组件使用非原始值作为hooks如useEffect,useMemo, useCallback的依赖项,它应该被缓存。
✔ 复杂计算使用useMemo
useMemo的使用场景之一是避免每次re-render时的复杂计算。
useMemo有自己的开销(消耗一点内存并且会使初始render稍微变慢),所以不应该每次运算都使用它。在React里,大多数情况下挂载(mounting)和更新组件是开销最大的计算(除非你在计算质数,当然你不应该在前端做这个)。
所以,useMemo典型的使用场景是存储React元素。通常是已存在的render树的一部分,或生成的render树的结果,比如一个返回新的元素的map函数。
与组件更新相比,"纯"JavaScript运算的开销例如对一个数组排序或过滤通常是微不足道的。
提升lists的re-render性能:
除了常规的re-render规则和模式,key属性会影响React中list的性能。
注意:仅提供key属性并不会提升list的性能。为防止list元素的re-render,你需要把他们包裹在React.memo里并且遵循所有最佳实践。
key的值应该是一个string,在re-render之间对list中的每个元素来说它都是一致的。通常使用item.id或array.index。
对于静态列表,即不会添加、删除、插入、重排元素,使用array.index是可以的。
动态list上使用array.index会导致:
- items有状态或任何非受控元素(例如input)时会有bug
- items被包裹在React.memo里的话性能会降低
更多细节可查看:www.developerway.com/posts/react...
❌反模式:lists里用随机值作为key
list里永远不应该用随机生成的值作为key。这会导致React每次re-render都会重新挂载(re-mounting)items,这会导致:
- list性能很差
- items有状态或任何非受控元素(例如input)时会有bug
阻止context造成的re-render
✔ 阻止context re-render: 缓存provider值
如果context provider没有被放在app的根节点,那么它有可能会因为祖先改变而re-render。它的值应该被缓存。
✔ 阻止context re-render: 分离data和API
如果一个context里存在data和API的组合(getter和setter),可以将它们分到同一组件下的不同provider里。这样的话,只使用API的组件不会随着数据的改变而re-render。
关于这个模式的更多信息:www.developerway.com/posts/how-t...
✔ 阻止context re-render: 分离data到chunks里
如果context管理一些独立的数据块,他们可以被分离到相同provider下更小的provider里。这样的话,发生变化的chunk的使用者才会re-render。
关于这个模式的更多信息:www.developerway.com/posts/how-t...
✔ 阻止context re-render: context selector
没有办法阻止使用了部分context值的组件re-render,即使使用部分的数据没有改变,即使有useMemo hook也没用。
然而我们可以使用高阶组件和React.memo伪造context seletor。
关于这个模式的更多信息:www.developerway.com/posts/highe...