关于 React 性能优化的文章有很多。通常,如果某些状态更新很慢,你需要做以下几件事:
- 确认你正在运行的是生产版本。(开发版本故意较慢,在极端情况下甚至会慢一个数量级。)
- 确认你没有把状态提升到不必要的位置。(例如,把输入状态放在集中存储中可能不是最好的主意。)
- 运行 React DevTools Profiler 查看哪些组件重新渲染,并用
memo()包裹最耗时的子树。(在需要的地方添加useMemo()。)
最后一步令人烦恼,特别是对于中间的组件,理想情况下编译器可以为你处理这些问题。未来可能会实现。
在这篇文章中,我想分享两种不同的技术。 它们出乎意料的基础,因此人们很少意识到它们能提高渲染性能。
这些技术是对你已经知道的技术的补充! 它们不能取代 memo 或 useMemo,但你可以先尝试一下。
一个(人为的)慢组件
这里有一个严重渲染性能问题的组件:
1 | import { useState } from 'react'; |
这里的问题是,每当 color 在 App 内部发生变化时,我们都会重新渲染 <ExpensiveTree /> ,我们人为地将其延迟得非常慢。
当然我可以把 memo() 放在上面,然而也就到此为止了,有很多关于它的现有文章,所以我不会花时间在它上面,我想展示两种不同的解决方案。
解决方案 1:下移状态
如果您仔细查看渲染代码,您会发现返回的树中只有一部分实际上关心当前的 color :
1 | export default function App() { |
因此,让我们将该部分提取到 Form 组件中,并将状态向下移动到其中:
1 | export default function App() { |
现在,如果 color 发生更改,则只有 Form 会重新渲染。问题解决了。
解决方案 2:提升内容
如果状态块在昂贵的树之上的某个地方使用,则上述解决方案不起作用。例如,假设我们将 color 放在父级 <div> 上:
1 | export default function App() { |
现在看来我们不能只是将不使用 color 的部分“提取”到另一个组件中,因为这将包括父 <div> ,然后父组件将包括 <ExpensiveTree /> 。这次不能回避 memo 了吧?
真的不可以吗?
答案其实非常简单:
1 | export default function App() { |
我们将 App 组件分成两部分。依赖于 color 的部分以及 color 状态变量本身已移至 ColorPicker 中。
不关心 color 的部分留在 App 组件中,并作为 JSX 内容传递给 ColorPicker ,也称为 children
当 color 更改时, ColorPicker 重新渲染。但它仍然具有上次从 App 获得的相同 children 属性,因此 React 不会访问该子树。
因此, <ExpensiveTree /> 不会重新渲染。
有什么意义?
在应用 memo 或 useMemo 等优化之前,看看是否可以将变化的部分与不变的部分分开可能是有意义的。
这些方法的有趣之处在于它们本身与性能没有任何关系。使用 children 属性来拆分组件通常会使应用程序的数据流更易于跟踪,并减少通过树向下查找的属性数量。在这种情况下,提高性能只是锦上添花,而不是最终目标。
奇怪的是,这种模式还可以在未来释放更多性能优势。
例如,当服务器组件稳定并准备好采用时,我们的 ColorPicker 组件可以从服务器接收其 children 。整个 <ExpensiveTree /> 组件或其部分都可以在服务器上运行,甚至顶级 React 状态更新也会“跳过”客户端上的这些部分。
这是连 memo 都做不到的事情!但同样,这两种方法是互补的。不要忽视向下移动状态(并向上提升内容!)
这不是一个新的想法。这是 React 组合模型的自然结果。它很简单,但却被低估了,值得更多的关注。