性能优化
1. 跳过不必要的组件更新
1. PureComponent、React.memo
2. shouldComponentUpdate
3. useMemo、useCallback 实现稳定的 Props 值
如果传给子组件的派生状态或函数,每次都是新的引用,那么 PureComponent 和 React.memo 优化就会失效。所以需要使用 useMemo 和 useCallback 来生成稳定值,并结合 PureComponent 或 React.memo 避免子组件重新 Render。
一般场景:
- 内联的函数定义
- 内联的样式对象
4. 发布者订阅者跳过中间组件 Render 过程
5. 状态下放,缩小状态影响范围
如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,并将该状态移动到该组件内部。如下面的代码所示,虽然状态 color 只在 <input />
和 <p />
中使用,但 color 改变会引起 <ExpensiveTree />
重新 Render。
import { useState } from 'react'
export default function App() {
const [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() {
return <p>I am a very slow component tree.</p>
}
通过将 color 状态、<input />
和 <p />
提取到组件 Form 中,结果如下。
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
)
}
function Form() {
const [color, setColor] = useState('red')
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
)
}
这样调整之后,color 改变就不会引起组件 App 和 ExpensiveTree 重新 Render 了。
6. 列表项使用 key 属性
并不是在所有列表渲染的场景下,使用 ID 都优于使用索引
在常见的分页列表中,第一页和第二页的列表项 ID 都是不同,假设每页展示三条数据,那么切换页面前后组件 Render 结果如下。
<!-- 第一页的列表项虚拟 DOM -->
<li key="a">dataA</li>
<li key="b">dataB</li>
<li key="c">dataC</li>
<!-- 切换到第二页后的虚拟 DOM -->
<li key="d">dataD</li>
<li key="e">dataE</li>
<li key="f">dataF</li>
切换到第二页后,由于所有 <li>
的 key 值不同,所以 Diff 算法会将第一页的所有 DOM 节点标记为删除,然后将第二页的所有 DOM 节点标记为新增。整个更新过程需要三次 DOM 删除、三次 DOM 创建。
如果不使用 key,Diff 算法只会将三个 <li>
节点标记为更新,执行三次 DOM 更新。
尽管存在以上场景,React 官方仍然推荐使用 ID 作为每项的 key 值。其原因有两:
- 在列表中执行删除、插入、排序列表项的操作时,使用 ID 作为 key 将更高效。而翻页操作往往伴随着 API 请求,DOM 操作耗时远小于 API 请求耗时,是否使用 ID 在该场景下对用户体验影响不大。
- 使用 ID 做为 key 可以维护该 ID 对应的列表项组件的 State。举个例子,某表格中每列都有普通态和编辑态两个状态,起初所有列都是普通态,用户点击第一行第一列,使其进入编辑态。然后用户又拖拽第二行,将其移动到表格的第一行。如果开发者使用索引作为 key,那么第一行第一列的状态仍然为编辑态,而用户实际希望编辑的是第二行的数据,在用户看来就是不符合预期的。尽管这个问题可以通过将「是否处于编辑态」存放在数据项的数据中,利用 Props 来解决,但是使用 ID 作为 key 也许更方便。
7. useMemo 返回虚拟 DOM
利用 useMemo 可 以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
- 更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
- 更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值。
8. 跳过回调函数改变触发的 Render 过程
React 组件的 Props 可以分为两类。
- 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer 和 renderProps 函数。
- 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange
该优化思想应该通过 useMemo/React.memo 实现,且使用 useMemo 实现时也更容易理解。
9. Hooks 按需更新
如果自定义 Hook 暴露多个状态,而调用方只关心某一个状态,那么其他状态改变就不应该触发组件重新 Render。
export const useNormalDataHook = () => {
const [data, setData] = useState({ info: null, count: null })
useEffect(() => {
const timer = setInterval(() => {
setData((data) => ({
...data,
count: data.count + 1,
}))
}, 1000)
return () => {
clearInterval(timer)
}
})
return data
}