先看两个例子
- 当 Parent 中的 value 发生更新时,A、B、C 中哪些组件会发生 re-render?为什么?
function Parent() {
<Provider value={xxx}>
<A />
</Provider>
}
function A() {
return <B />
}
const B = React.memo(() => {
return <C />
})
function C() {
const ctx = useContext(xxx)
return <XXX />
}
- MemoizedComponentX 是使用 React.memo 包装的组件,当 Parent 发生 re-render 时,MemoizedComponent1、MemoizedComponent2、MemoizedComponent3、MemoizedComponent4 中哪些会发生 re-render?为什么?
function Parent(props) {
return <>
<MemoizedComponent1><OtherComponent /></MemoizedComponent1>
<MemoizedComponent2><div /></MemoizedComponent2>
<MemoizedComponent3>Hello</MemoizedComponent3>
<MemoizedComponent4>{props.children}</MemoizedComponent4>
</>;
}
function App() {
return <Parent>
<NormalComponent />
</Parent>
}
如果你能很快找出上例中哪些组件会发生 re-render,并且知道其发生 re-render 的原因,那后面的内容就无需再看啦。😄
Initial Render 与 Re-render
可以说,re-render 是 React 在 UI=f(State)
理念下让应用「可交互」的核心驱动力。
那什么是非必要的 re-render 呢?
如上图中,组件 B 的 state:foo 更新了,D 消费了 foo,其 re-render 是必要的,而 C、E、F、G 则是非必要的。
什么情况下会发生 re-render ?
- state 变化了
class People extends React.Component {
state = {
name: 'zhangsan'
}
render() {
return <h1>{this.state.name}</h1>
}
}
- hooks 变化了
function NetworkStatus() {
const status = useNetworkStatus();
return <h1>{status}</h1>
}
- 父组件 re-render 了
function Parent() {
const [name, setName] = useState(xx);
return <Child />;
}
function Child() {
return <h1>hello child</h1>
}
- props 变化了<仅当组件被 memo 时考虑>
function Parent() {
const [name, setName] = useState(xx);
return <Child name={name} />;
}
// Child 被 memo,不受 Parent re-render 影响
const Child = React.memo((props) => {
return <h1>{props.name}</h1>
})
- context 变化了
const People = React.meme(() => {
const name = useSelector(({ name }) => name);
return <h1>{name}</h1>
})
- 祖先组件有声明 childContextTypes,子孙组件订阅了其状态变更【已废弃特性】
有哪些优化 Re-render 的手段?
- 分离不会同时更新的 Context
Split contexts that don’t change together
当有 a、b、c 三个状态,a 与 b 更新频率不一样,a 与 c 则属于毫无关联的子业务,则这仨状态可以切分到不同的 Context,避免下层消费 Context 时由于并不关注的状态更新而触发非必要的 re-render。
- 为 Provider 的 children 提供 memo,或使用 props.children,以避免 Context 更新时造成子树递归 re-render
- 为 Provider 的 children 添加 memo:
function Parent() {
<Provider value={xxx}>
<A />
</Provider>
}
// 这里中断了 Provider 更新触发的子树递归更新
const A = React.memo(() => {
return <B />
})
// 由于 A 被 memo,B 不会 re-render
const B = () => {
return <C />
}
// C 消费了 Context,re-render
function C() {
const ctx = useContext(xxx)
return <XXX />
}
- 直接使用 props.children
function Parent() {
<Provider value={xxx}>
<A />
</Provider>
}
// 这里中断了 Provider 更新触发的子树递归更新
const A = React.memo(() => {
return <B />
})
// 由于 A 被 memo,B 不会 re-render
const B = () => {
return <C />
}
// C 消费了 Context,re-render
function C() {
const ctx = useContext(xxx)
return <XXX />
}
- 避免使用 Context 传递频繁更新的值
如果通过 Context 传递的是更新频率很低甚至不更新的值,那上面两项优化都没必要做了。
react-redux 、jotai 等状态管理工具中通过 Context 传递的都是静态对象。
数据更新
非 react life cycle 中,手动合并 update (react 18 之前)
Automatic batching for fewer renders in React 18
数据订阅
- 按需订阅数据
- selector
仅 select 需要的状态,清理不再使用的状态:
function A() {
// y 未被使用
const { x, y } = useSelector(({ x, y }) => ({ x, y }), shallowEqual);
return <>
<B x={x} />
....
<Other />
</>
}
- 直接使用原子状态,不需要「样板式 select」
以 jotai 为例
function A() {
const [name, setName] = useAtom(nameAtom)
return <h1>{name}</h1>
}
- 更细粒度的数据订阅
基于 Proxy 搜集所需依赖,以 valtio 为例
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ name: '张三', age: 18 });
function A() {
const snap = useSnapshot(state);
// 在 name 为 '李四' 前,`age` 的变化不会触发组件 re-render
return <>
<p>{snap.name}</p>
{snap.name === '李四' ? <p>{snap.age}</p> : null}
</>
}
- 拆分更细粒度的组件:
function A() {
// x 频繁更新,则不宜放在一个 render 成本很高的组件内直接消费
const { x, y } = useSelector(({ x, y }) => ({ x, y }), shallowEqual);
return <>
<B x={x} />
<C y={y} />
....
<Other />
</>
}
- 等等等等…都快召唤神龙[手动狗头]了怎么还没有 memo、useCallback、useMemo?
何时该用 xxx
脆弱的 memo 与 shouldComponentUpdate 还有那 PureComponent
PureComponent 是 shouldComponentUpdate 的阉割简化版,而 memo 则是弥补 Function Component 下没有 shouldComponentUpdate 而增加的高阶函数。
通过这些手段试图“记忆”组件的方式很容易被破坏:
// 案例一
function App() {
return <MemoComponent1 onChange={() => {}} />
}
// 案例二
function App() {
return <MemoComponent2>
<OtherComponent />
</MemoComponent2>
}
当 App 发生 re-render 时,无论 MemoComponent1 还是 MemoComponent2 的“记忆”均会失效,为什么呢?
- 案例一 :onChange 在 App 每次 re-render 时会重新创建为新的 Function,props.onChange 发生了更新
- 案例二:OtherComponent 在 App 每次 re-render 时会重新创建(想象下 JSX 编译为 React.createElement 每次执行返回新对象),props.children 发生了更新
这时你可能会犀利地发问了:这几个 API 提供的优化能被如此轻易破坏,你当 React 团队是干什么吃的?
当时我大意了,没有闪,但依旧反手丢出了一坨代码:
// 案例一
function App() {
const handleChange = useCallback(() => {
// ...
}, [])
return <MemoComponent1 onChange={handleChange} />
}
// 案例二
function App() {
<MemoComponent2>
{useMemo(() => <OtherComponent />, [])}
</MemoComponent2>
}
你很不屑地嘀咕道:“不讲武德”。并随之厉声直击我痛处:指望你的组件使用方一直提供稳定的 props 么?
这里其实引出了另一个问题,该不该依靠外部传入稳定的 props 以保证组件的 memo 生效?
我觉得是不应该的。
作为组件的使用方,不应该感知组件的实现,不感知组件实现也就无法知道组件使用了 memo,也就不会主动确保 props 的稳定了。
那为什么不约定成每次都使用 useMemo、useCallback 包装后再传递 props 呢?
- 造成 useCallback / useMemo hell
- 对于非 memo 的组件无任何益处
- 使用 useCallback / useMemo 的开销在很多情况下大于“裸写”
function App() {
const handleChange = useCallback(() => {
// App re-render 时,这个函数在内存里会存在两份
}, []);
// 最大的误用:未被 memo 的 Component 依旧会 re-render(App re-render 了)
return <Component onChange={handleChange} />
}
何时该 memo 组件
一句话:把复杂留给自己
如果组件内(上图 B、C)存在计算量大的组件,从而不希望该组件出现“非必要的 re-render”,应该在组件内部处理好何时该 re-render 的问题,这样对于 A 使用 B 抑或 B 使用 C 都无需再担心“非必要 re-render“带来性能问题了:
const InnertExpensiveComponent = React.memo(() => {
// ...
});
export function MyComponent() {
return <InnertExpensiveComponent />
}
何时该用 useMemo、useCallback
- useMemo
// 案例一
function Foo() {
const memoizedValue = useMemo(() => computeExpensiveValue(), [])
return <XXX />
}
// 案例二
const Bar = ({ a }) => {
const memoObj = useMemo(() => {
return {
a,
//....
}
}, [a])
useEffect(() => {
// do something with memoObj
}, [memoObj]);
};
- useCallback
export function Foo() {
const handleOnChange = useCallback(() =>{}, [])
return <InnertExpensiveComponent onChange={handleOnChange} />
}
const InnertExpensiveComponent = React.memo(({ onChange }) => {
// ...
});
结语
过早地介入优化“非必要的 re-render” ROI 可能并不高,优先解决慢的 render 过程收益更大
Fix the slow render before you fix the re-render
在大多数情况下,组件的非必要 re-render 并不会造成性能问题,React 与浏览器能非常迅速处理该过程。
但如果 re-render 过于频繁地发生在拥有复杂计算的组件上时,可能在 re-render 过程中造成应用变得不可交互甚至完全卡死,此时可使用 React Developer Tools 定位 render 成本很高的组件进行优化。