有很多描写 React 性能优化的文章。一般而言,如果某些 state 更新缓慢的话,你需要:

  1. 验证是否正在运行一个生产环境的构建。(开发环境构建会刻意地缓慢一些,极端情况下可能会慢一个数量级)
  2. 验证是否将树中的状态放在了一个比实际所需更高的位置上。(例如,将输入框的 state 放到了集中的 store 里可能不是一个好主意)
  3. 运行 React 开发者工具来检测是什么导致了二次渲染,以及在高开销的子树上包裹memo()。(以及在需要的地方使用useMemo()

最后一步是很烦人的,特别是对于介于两者之间的组件,理想情况下,编译器可以为您完成这一步。未来也许会。 在这篇文章里,我想分享两种不同的技巧。 它们十分基础,这也正是为什么人们很少会意识到它们可以提升渲染性能。 这些技巧和你已经知道的内容是互补的 它们并不会替代memo 或者 useMemo,但是先试一试它们还是不错的

一个(人工)减缓的组件

这里是一个具有严重渲染性能问题的组件

  1. import { useState } from 'react';
  2. export default function App() {
  3. let [color, setColor] = useState('red');
  4. return (
  5. <div> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> <ExpensiveTree /> </div>
  6. );
  7. }
  8. function ExpensiveTree() {
  9. let now = performance.now();
  10. while (performance.now() - now < 100) {
  11. }
  12. return <p>I am a very slow component tree.</p>;
  13. }

(在这里试试)

问题就是当App中的color变化时,我们会重新渲染一次被我们手动大幅延缓渲染的<ExpensiveTree />组件。 我可以直接在它上面写个 memo()然后收工大吉,但是现在已经有很多这方面的文章了,所以我不会再花时间讲解如何使用 memo() 来优化。我只想展示 两种不同的解决方案。

解法 1: 向下移动 State

如果你仔细看一下渲染代码,你会注意到返回的树中只有一部分真正关心当前的color

  1. export default function App() {
  2. let [color, setColor] = useState('red'); return (
  3. <div>
  4. <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> <ExpensiveTree />
  5. </div>
  6. );
  7. }

所以让我们把这一部分提取到Form组件中然后将 state 移动到该组件里:

  1. export default function App() {
  2. return (
  3. <>
  4. <Form /> <ExpensiveTree />
  5. </>
  6. );
  7. }
  8. function Form() {
  9. let [color, setColor] = useState('red'); return (
  10. <>
  11. <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> </>
  12. );
  13. }

(在这里试试)

现在如果color变化了,只有Form会重新渲染。问题解决了。

解法 2:内容提升

当一部分 state 在高开销树的上层代码中使用时上述解法就无法奏效了。举个例子,如果我们将color放到父元素div中。

  1. export default function App() {
  2. let [color, setColor] = useState('red'); return (
  3. <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} />
  4. <p>Hello, world!</p>
  5. <ExpensiveTree />
  6. </div>
  7. );
  8. }

(在这里试试)

现在看起来我们似乎没办法再将不使用color的部分提取到另一个组件中了,因为这部分代码会首先包含父组件的div,然后才包含 <ExpensiveTree />。这时候无法避免使用memo了,对吗?又或者,我们也有办法避免?

在沙盒中玩玩吧,然后看看你是否可以解决。 …

答案显而易见:

  1. export default function App() {
  2. return (
  3. <ColorPicker>
  4. <p>Hello, world!</p> <ExpensiveTree /> </ColorPicker>
  5. );
  6. }
  7. function ColorPicker({ children }) { let [color, setColor] = useState("red");
  8. return (
  9. <div style={{ color }}>
  10. <input value={color} onChange={(e) => setColor(e.target.value)} />
  11. {children} </div>
  12. );
  13. }

(在这里试试)

我们将App组件分割为两个子组件。依赖color的代码就和color state 变量一起放入ColorPicker组件里。 不关心color的部分就依然放在App组件中,然后以 JSX 内容的形式传递给ColorPicker,也被称为children属性。 当color变化时,ColorPicker会重新渲染。但是它仍然保存着上一次从App中拿到的相同的children属性,所以 React 并不会访问那棵子树。 因此,ExpensiveTree不会重新渲染。

寓意是什么?

在你用memo或者useMemo做优化时,如果你可以从不变的部分里分割出变化的部分,那么这看起来可能是有意义的。 关于这些方式有趣的部分是他们本身并不真的和性能有关. 使用 children 属性来拆分组件通常会使应用程序的数据流更容易追踪,并且可以减少贯穿树的 props 数量。在这种情况下提高性能是锦上添花,而不是最终目标。 奇怪的是,这种模式在将来还会带来更多的性能好处。 举个例子,当服务器组件 稳定且可被采用时,我们的ColorPicker组件就可以从服务器上获取到它的children。 整个<ExpensiveTree />组件或其部分都可以在服务器上运行,即使是顶级的 React 状态更新也会在客户机上 “跳过” 这些部分。 这是memo做不到的事情! 但是,这两种方法是互补的。不要忽视 state 下移 (和内容提升!) 然后,如果这还不够,那就使用 Profiler 然后用 memo 来写吧。

我之前不是读过这个吗?

大概是的吧

这并不是一个新想法。这只是一个 React 组合模型的自然结果。它太简单了以至于得不到赏识,然而它值得更多的爱。
https://overreacted.io/zh-hans/before-you-memo/