关于 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 组合模型的自然结果。它很简单,但却被低估了,值得更多的关注。