什么是重新渲染(re-render)
注意这里的渲染(render)其实是 react 中的协调(reconciliation) 过程,对比计算虚拟 DOM 树的差异,等到提交(commit)阶段更新到视图。
渲染分两种:初次渲染和重新渲染,什么情况下会触发组件的重新渲染,主要分为两种情况:
- 组件自身状态的变化(
useState
、useReducer
、useContext
) - 父级组件引起的重新渲染(props变化也属于这种情况)
props变化并不会引起子组件重新渲染,而根本原因就是当父组件渲染时,其所有的子组件都会重新渲染。
这里会有一个问题就是,一些情况下组件的重新渲染是没必要的,比如子组件并没有使用父组件的状态作为 props 或者 props 并没有更新,但父组件的重新渲染还是会导致子组件的重新渲染。
reconciliation 是从 root 开始,但会跳过所有父节点,到 state 发生变化的组件开始往下重新渲染。
不必要的重新渲染一般情况下不是问题,其性能开销并不大;在没有明显感知的卡顿情况下,可以不必进行优化。相反使用 React 提供的 memoization 相关的api造成的性能消耗可能会大于组件的重新渲染,得不偿失。
Don't optimize prematurely!(不要过早的性能优化)
但如果出现了性能问题,比如重新渲染触发频繁或者在性能开销大的组件上,则就必须用一些手段去跳过不必要的重新渲染。
举个例子,如Content
组件内部有状态进行了更新,则Content
会重新渲染,以及其所有的子组件会重新渲染。但可能 Tree
其实并不需要重新渲染,且这个组件渲染耗时较大,这时就需要想办法跳过 Tree
的 re-render。
text
App
/ \
Header Content ←----- update
/ \
Tree Table
优化手段
这里使用dan的一个例子,ExpensiveTree
是比较耗时的组件,当App
组件修改颜色时,会导致App
重新渲染,ExpensiveTree
且没有依赖父组件的状态,是不要重新渲染的。
jsx
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() {
const now = performance.now();
while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
}
下面介绍一些优化手段:
使用memo
将ExpensiveTree
使用memo
函数包裹,会对props
进行浅比较,如果有变化才更新。
jsx
const ExpensiveTree = memo(() => {
const now = performance.now();
while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
});
注意使用memo是避免父组件引发的重新渲染,如果组件自身状态发生变化,则不受影响
类组件可以查看 PureComponent
/shouldComponentUpdate
下放state
- 一般情况下可通过封装组件解决,将变化的state封装到子组件,变成兄弟组件就不会相互影响了。找个方法就是要求对组件进行合理粒度的拆分和封装。
jsx
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>
</>
);
}
const ExpensiveTree = memo(() => {
...
});
组件作为props
- 将不需要渲染的组件作为
children
传递
jsx
export default function App() {
return (
<ColorPicker>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }) {
let [color, setColor] = useState("red");
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
{children}
</>
);
}
color
变化时,ColorPicker
会重新渲染,而ExpensiveTree
作为App
的子组件通过props.children
进行传递, 引用并没有变化。
将组件作为
props.xxx
传递也是一样的,children
其实可以看作特殊的props
优化手段总结
组合
- 下放state
- 组件作为
children
/props
传递
使用 memo
- 如果
props
是非基本类型的,需要配合使用useMemo
/useCallback
将传递的值进行缓存,防止引用变化
Context 相关
jsx
function MyPage() {
const [theme,setTheme] = useState('dark')
return (
<Context.Provider value={theme}>
<Header />/
<Content />
</Context.Provider>
);
}
注意,只有在Context.Provider
重新渲染的情况下,才会去对比value
的变化,如果value
的值发生变化,会使消费Context
的相应组件重新渲染。
在上面的例子看,使用 state 作为value
,通过 setter 触发,会使MyPage
重新渲染,会导致所有子组件重新渲染,不论子组件是不是使用useContext
。这里可以使用上文的优化措施,避免不必要的子组件渲染。
优化手段:
- 如果不是在根组件使用
Context.Provider
,父组件可能导致其重新渲染的话,可使用useMemo
记住value
的值,防止value
的引用变化 - 进行
Context
的拆分,将不变的Context
向外移,将变化的Context
向内移动 - 使用
selectors
,通过选择需要的状态从而规避掉无关的状态改变时带来的渲染开销。