前言
说到性能优化,大家一定会想到PureComponent、memo、useMemo、useCallback这些api,比如useMemo,我传入一个依赖项,敌不动我不动,那些复杂计算结果就可以被缓存下来,其实岁月静好是因为有人替你负重前行,哈哈哈。
相比Vue可以基于模版进行编译时性能优化,React作为一个完全运行时的库,只能在运行时谋求性能优化。
正题
不使用api
假如我有一个包含子组件B的父组件A,B是一个不依赖父组件props的组件,父组件A中包含修改自身状态的逻辑,那么每当A组件状态改变,就会带动我不变的B组件更新,如果B组件规模较大,就非常消耗性能。
export default function Optimization() {
const [num, updateNum] = useState(0)
return (
<div>
<input value={num} onChange={updateNum(num += 1)} />
<p>num is {num}</p>
<Expensive />
</div>
)
}
function Expensive(){
let now=performance.now()
while(performance.now()-now<100){}
return <p>耗时的组件</p>
}
我们可以在不使用性能优化api的情况下,将变得部分从不变的分出去。
export default function Optimization() {
return (
<div>
<Input/>
<Expensive />
</div>
)
}
function Input() {
const [num, updateNum] = useState(0)
return (
<>
<input value={num} onChange={updateNum(num += 1)} />
<p>num is {num}</p>
</>
)
}
function Expensive() {
let now = performance.now()
while (performance.now() - now < 100) { }
return <p>耗时的组件</p>
}
书写也更规范了,有效避免造s山哈哈哈。
使用api
- purecomponent和memo:内部is方法对props浅比较,有一定效果。
- useMemo和useCallback:如果传递的是引用类型,浅比较可能失效,需要用这类api进行缓存。
以上就是api的效果。
他们似乎都围绕着一些东西在操作,没错,就是围绕react内部的性能优化策略:eager state和bailout
React的工作流程可以简单概括为:
- 交互(比如点击事件、useEffect)触发更新
- 组件树render
当你更新后的内容与之前无异,就命中eager statet策略,跳过了步骤2。
bailout策略是对于父组件来说的,父组件render之后发现和之前的state相比并没有改变,那么它的子组件就不会被更新,这一过程发生在父组件的步骤2
关于完整的bailout策略过程
- 父组件render返回的jsx与current fiber tree进行diff,创建workInProgress fiber tree
- 命中bailout策略:不执行render复用current fiber tree;没有命中:current fiber tree=workInProgress fiber tree
- render后发现无需更新,然后执行bailout的情况:如shouldComponentUpdate
- fiber的子树上有work:继续遍历子树;没有:return null
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// 重用以前的context依赖关系
workInProgress.dependencies = current.dependencies;
}
// 检测子树(childLanes)是否有work
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
//无,跳过子树
return null;
}
// 说明子树有work,继续遍历子树
// workInProgress.child 转化为 workInProgress
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
执行时机
render前判断
//删除部分代码
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 是update阶段,
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
// props不同 需要update
oldProps !== newProps ||
// 旧版上下文(现在不使用)
hasLegacyContextChanged() ||
// dev时,会判断type,用于热重载
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 需要更新。 (不代表必定更新,例如memo)
didReceiveUpdate = true;
} else {
// 此fiber是否有lanes更新任务
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
if (
// 没有updateLane
!hasScheduledUpdateOrContext &&
// 没有Suspend,错误边界的传递.
(workInProgress.flags & DidCapture) === NoFlags
) {
didReceiveUpdate = false;
// 无更新, 尝试bailout.
// 多数组件会直接进行baliout。
// Suspend、Offscreen组件可能不会执行bailout。
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
// 有更新任务,但是props没改变,先设置需要更新为false
// 组件后续进行确定更新时,会将其设置为true
didReceiveUpdate = false;
}
} else {
// mount
}
// ...非bailout代码
}
在beginWork中,当前处于update(有current fiber了)&props不变(===)&没有updateLanes&没有Suspend&NoFlags,尝试进行bailout。
render之前!!!每次render后都会createElement,创建新props对象,就无法命中bailout了。
多数组件可以满足以上条件。
ps:context这种小弟也可能影响命中。
lane无任务
fiber.lane无任务,继续进行bailout
function checkScheduledUpdateOrContext(current: Fiber,renderLanes: Lanes,): boolean {
// 检查fiber.lanes是否有任务
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
// ... lazy context 逻辑。
// ... lazy context还在测试,并没有启用。
return false;
}
(workInProgress.flags & DidCapture) === NoFlags
没有suspend和error,可以开始bailout
render后判断
render后发现无需更新的情况,尝试bailout,减少子树render
ClassComponent
// 是否需要update,删除非update的代码
function updateClassInstance (){
//...
// 是否需要更新
const shouldUpdate =
// 是否有ForceUpdate
checkHasForceUpdateAfterProcessing() ||
// 检测组件是否要更新
// 1. 若有shouldComponentUpdate则由其控制。
// 2. 若是PureComponent,则判断props、state是否equeal。
// 3. 无shouldComponentUpdate也不是PureComponent则需要更新。
checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
)
//... 随后执行finishClassComponent
return shouldUpdate
}
// bailout逻辑,删除非bailout的代码
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
// ...
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
// 不需要更新,且 没有错误边界
if (!shouldUpdate && !didCaptureError) {
// bailout
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// ...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
无forceUpdate&组件无需更新时,触发bailout。
组件是否需要更新按照以下逻辑判断
- 有shouldComponentUpdate,由它控制
- 如果基类是PuerComponent,判断props&state是否equal
- 没有sCU和PC,就需要被更新
FunComponent
// 是否需要update,删除非update的代码
function updateClassInstance (){
//...
// 是否需要更新
const shouldUpdate =
// 是否有ForceUpdate
checkHasForceUpdateAfterProcessing() ||
// 检测组件是否要更新
// 1. 若有shouldComponentUpdate则由其控制。
// 2. 若是PureComponent,则判断props、state是否equeal。
// 3. 无shouldComponentUpdate也不是PureComponent则需要更新。
checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
)
//... 随后执行finishClassComponent
return shouldUpdate
}
// bailout逻辑,删除非bailout的代码
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
// ...
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
// 不需要更新,且 没有错误边界
if (!shouldUpdate && !didCaptureError) {
// bailout
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// ...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
update&组件无需更新时,命中bailout。
example
function A() {
const [v, setV] = useState(1)
console.log('A')
return <div onClick={() => setV(2)}><AA/></div>
}
function AA() {
console.log('AA')
return 'AA'
}
第一次:打印A ,AA
第二次:打印A,子组件命中bailout
第三次:啥也不打印,俩人都没变
MemoComponent
分为SimpleMemo 组件和Memo组件
updateSimpleMemoComponent
// SimpleMemoComponent逻辑
function updateSimpleMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
if (current !== null) {
const prevProps = current.memoizedProps;
if (
// props 浅比较equal
shallowEqual(prevProps, nextProps) &&
// ref全等
current.ref === workInProgress.ref &&
// 用于热重载
(__DEV__ ? workInProgress.type === current.type : true)
) {
didReceiveUpdate = false;
// fiber.lanes没有任务
if (!checkScheduledUpdateOrContext(current, renderLanes)) {
workInProgress.lanes = current.lanes;
// bailout执行
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
}
}
}
// 不能bailout,进入FC
return updateFunctionComponent(current,workInProgress,Component,nextProps,renderLanes);
}
updateMemoComponent
// 已删除与bailout逻辑不相干代码
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
if (current === null) {
const type = Component.type;
// 简单memo组件,非class组件,没有compare,没有默认props,
if (
isSimpleFunctionComponent(type) &&
Component.compare === null &&
Component.defaultProps === undefined
) {
let resolvedType = type;
// fiber标记为SimpleMemoComponent
// 此fiber后续,走updateSimpleMemoComponent函数,不在进入此函数。
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
// ... 创建SimpleMemo组件
return updateSimpleMemoComponent();
}
// ... mount逻辑
return child;
}
const currentChild = ((current.child: any): Fiber);
// 检测是否有updateLanes任务,逻辑在上面有写。
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
// 无更新任务
if (!hasScheduledUpdateOrContext) {
const prevProps = currentChild.memoizedProps;
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
// caompare对比相同 且 ref全等
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
// 进行bailout
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// ... 创建newChild
const newChild = createWorkInProgress(currentChild, nextProps);
return newChild;
}
ContextComponent
使用了context的组件必定随着context更新,不受性能优化api影响。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
if (enableLazyContextPropagation) {
// .. 无逻辑
} else {
// oldProps不为null,保证必须传入value
if (oldProps !== null) {
const oldValue = oldProps.value;
// 新旧value通过Object.is比较。 比全等严格。
if (is(oldValue, newValue)) {
if (
// children全等
oldProps.children === newProps.children &&
// 旧版本的context没有变化
// 旧版本的context不使用了,认为无变化即可。
!hasLegacyContextChanged()
) {
// bailout
return bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes);
}
} else {
// 注意:会给所有使用此context的子组件,安排一个lanes任务
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
oldProps!==null&is(oldValue,newValue)&oldProps.children===newProps.children时命中bailout。
性能优化应该遵循的步骤
- 寻找项目中性能损耗严重的子树
- 在子树的根节点使用性能优化api
- 在子树中采用变与不变分离的策略
总结
- 组件内state、context的更新都会引起整个子组件rerender(可尝试使用MemoComponent减少render)
- 减少props属性改变,api及生命周期才能起作用(在子组件中使用它们)
- ContextProvider会对比children、value,所以子组件尽量用children传递 保证不变