很多文章都写过 React 的性能优化,一般来说,当你的 state 更新缓慢的时候,你需要
- 确认是否在生产环境(开发环境通常具有开发相关的依赖,有时候会慢一个数量级)
- 确认是否把 state 放在一棵复杂结构并且在高位的树。(例如把 Input state 放在一棵树上并不是一个好主意)
- 使用 React DevTools 分析器查看重渲染过程,在性能消耗最多的子树使用 memo() 包裹。
最后一步非常的烦人,尤其是组件在中间的时候,在未来,这一步可能可以让编译器自动解决。
在这篇文章中,我想分享两个不同并且非常简单的优化渲染性能的方案。
这些方法你一定知道,它们没打算取代 memo() 或者 useMemo() 的地位,但是非常值得优先考虑。
一个很慢的组件
这里有一个有非常严重的渲染性能的组件
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>;
}
这个组件的问题是 color 在 App 里改变的时候 , 重渲染 <ExpensiveTree /> 组件会非常的慢
我可以给他包裹上 memo() 然后这个组件就会被缓存起来(因为他的state并没有改变),这种方案已经很成熟,所以我不打算花时间在它身上。下面给出两种方案。
方案1:下移 state
如果你仔细看了这个示例代码, 你会发现,只有使用了 color 变量的地方才是需要重渲染的地方
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 /> // 这一行与 `color` 无关
</div>);
}
所以,我们可以把 color 影响到的部分抽取出来,放到 Form 组件里面
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 改变的时候, <ExpensiveTree /> 并不会重渲染
方案2:提升内容
上面的方法在父元素都使用 color 变量时不生效,例如,我们把 color 也传递给 div
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> );
}
这样我们无法下移 state 了,因为父元素也使用了 color, 看起来我们无法避免要使用 memo() 了?
或者我们可以使用别的?
答案非常简单
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 影响的范围,把该范围内的都抽取到 <ColorPicker /> 中
另一部分不在color范围内的内容我们作为children 传递给<ColorPicker />
当color改变的时候,<ColorPicker />会重渲染,但是他的 children 依然是不变的,React 会忽略这部分子树的渲染过程,采用上一次渲染中的内容去填充
结果就是 <ExpensiveTree /> 不会被重渲染
启示
在你应用 memo 或者 useMemo,之前,可能需要检查一下,看下你是否可以把组件分割成 state 影响的部分和state忽略的部分
有趣的是,这些方法实际上都并没有对性能优化做过什么操作,使用传递children 这样的方式分割组件, 通常都会使你的应用的数据流更容易跟踪,并减少通过树向下传递的属性数量。这种情况下,性能优化只是顺带的,并不是我们的目的
奇怪的是,这种模式还会给未来解锁更多的性能好处
例如, 当服务端的组件被采用的时候,ColorPicker 组件可以把服务端的组件作为 children。无论是整个 <ExpensiveTree /> 组件或是运行在服务端的一部分,就算是顶层的 state 更新, 也会跳过相应的部分渲染。
这就算是memo() 也是做不到的,重申一下,两个方案是相辅相成的,不要忽视状态下移,还有内容提升。
这不是一个新想法。它是 React 组合模型的自然结果。它很简单,以至于被低估了,值得更多一点的关注。