在你使用 memo() | useMemo()之前...
小举 筑基

关于 React 性能优化的文章有很多。通常,如果某些状态更新很慢,你需要做以下几件事:

  1. 确认你正在运行的是生产版本。(开发版本故意较慢,在极端情况下甚至会慢一个数量级。)
  2. 确认你没有把状态提升到不必要的位置。(例如,把输入状态放在集中存储中可能不是最好的主意。)
  3. 运行 React DevTools Profiler 查看哪些组件重新渲染,并用 memo() 包裹最耗时的子树。(在需要的地方添加 useMemo()。)

最后一步令人烦恼,特别是对于中间的组件,理想情况下编译器可以为你处理这些问题。未来可能会实现。

在这篇文章中,我想分享两种不同的技术。 它们出乎意料的基础,因此人们很少意识到它们能提高渲染性能。

这些技术是对你已经知道的技术的补充! 它们不能取代 memouseMemo,但你可以先尝试一下。

一个(人为的)慢组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState } from 'react';

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

function ExpensiveTree() {
let now = performance.now();
while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
}

这里的问题是,每当 colorApp 内部发生变化时,我们都会重新渲染 <ExpensiveTree /> ,我们人为地将其延迟得非常慢。

当然我可以把 memo() 放在上面,然而也就到此为止了,有很多关于它的现有文章,所以我不会花时间在它上面,我想展示两种不同的解决方案。

解决方案 1:下移状态

如果您仔细查看渲染代码,您会发现返回的树中只有一部分实际上关心当前的 color

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

因此,让我们将该部分提取到 Form 组件中,并将状态向下移动到其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
);
}

function Form() {
let [color, setColor] = useState('red');
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
);
}

现在,如果 color 发生更改,则只有 Form 会重新渲染。问题解决了。

解决方案 2:提升内容

如果状态块在昂贵的树之上的某个地方使用,则上述解决方案不起作用。例如,假设我们将 color 放在父级 <div> 上:

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

现在看来我们不能只是将不使用 color 的部分“提取”到另一个组件中,因为这将包括父 <div> ,然后父组件将包括 <ExpensiveTree /> 。这次不能回避 memo 了吧?

真的不可以吗?

答案其实非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function App() {
return (
<ColorPicker>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorPicker>
);
}

function ColorPicker({ children }) {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}

我们将 App 组件分成两部分。依赖于 color 的部分以及 color 状态变量本身已移至 ColorPicker 中。

不关心 color 的部分留在 App 组件中,并作为 JSX 内容传递给 ColorPicker ,也称为 children

color 更改时, ColorPicker 重新渲染。但它仍然具有上次从 App 获得的相同 children 属性,因此 React 不会访问该子树。

因此, <ExpensiveTree /> 不会重新渲染。

有什么意义?

在应用 memouseMemo 等优化之前,看看是否可以将变化的部分与不变的部分分开可能是有意义的。

这些方法的有趣之处在于它们本身与性能没有任何关系。使用 children 属性来拆分组件通常会使应用程序的数据流更易于跟踪,并减少通过树向下查找的属性数量。在这种情况下,提高性能只是锦上添花,而不是最终目标。

奇怪的是,这种模式还可以在未来释放更多性能优势。

例如,当服务器组件稳定并准备好采用时,我们的 ColorPicker 组件可以从服务器接收其 children 。整个 <ExpensiveTree /> 组件或其部分都可以在服务器上运行,甚至顶级 React 状态更新也会“跳过”客户端上的这些部分。

这是连 memo 都做不到的事情!但同样,这两种方法是互补的。不要忽视向下移动状态(并向上提升内容!)

这不是一个新的想法。这是 React 组合模型的自然结果。它很简单,但却被低估了,值得更多的关注。