第 24 章:bailout 与优化——memo、自动 memo 与 React Compiler
React 性能优化的本质,一句话:让更多子树跳过重算。跳过这件事叫 bailout,第 6 章介绍过。本章把 bailout 的三个条件、两层时机讲透,再回答"为什么 React Compiler 能自动做这些"。
先记住这一句:bailout 需要三个条件同时满足:本组件 props 没变、自身没有更新、子树没有活(childLanes)。memo 是"手动告诉 React 满足条件一",React Compiler 是"构建期自动帮你加"。
一图流:要不要重算的决策树
bailout 的三条件
第 6 章讲过 beginWork 开头的检查,它对应三个条件(ReactFiberBeginWork.js:4113):
- props 没变:
oldProps === newProps。 - 自身没更新:
checkScheduledUpdateOrContext返回 false(没有 setState、context 没变)。 - 子树没活:
childLanes里没有当前渲染相关的 lane(第 6 章那个"预约登记")。
三个都满足,attemptEarlyBailoutIfNoScheduledUpdate(3845)直接返回,最后落到 3714,整棵子树跳过。任何一个不满足,就得进 updateXxx 重算。
注意"子树没活"是那个反向条件:props 没变但子组件自己 setState 了,父组件照样不重算,但会钻下去让子组件算。所以 bailout 是"本组件省了",不是"整棵都省了"。
memo:手动满足条件一
memo 在第 10 章讲过:它给组件加一层 shallowEqual 比较,props 相等就直接 bailoutOnAlreadyFinishedWork(472)。它干的事就是把"props 没变"这个本来靠 React 猜的条件,变成显式保证。
为什么 memo 有用?因为父组件重渲染时,它会用新的 props 对象调子组件,oldProps !== newProps 恒为 true,bailout 条件一永远不满足。memo 用浅比较帮 React"看穿":值没变,引用变了没关系,照样跳过。所以 memo 只对"props 频繁变引用但值不变"的场景有收益,无脑包 memo 反而白白多一次浅比较。
自动 memo:React Compiler
React Compiler 是构建期工具(Babel 插件),它把"手动包 memo"这件事自动化:编译时分析每个组件读什么、写什么,自动生成 useMemo、useCallback 和 React.memo 的等价代码,只有"真的可能被影响"的值才重算。
它跟运行时关系不大,本质是把你会在第 10、15 章手写的优化,提前到编译期做。好处是开发时不用再纠结 memo 该不该包,坏处是编译器必须足够保守,分析不出依赖就多缓存。这也是为什么"依赖不纯、乱改 props"的代码会让 Compiler 失效。
实践顺序
- 先靠默认 bailout:别乱包 memo。大部分优化靠"父组件 props 传递稳定"就能吃到默认 bailout。
- 定位再手动:用 DevTools 的 Profiler 找到"总是重算"的热点,再针对性地 memo / 稳定引用。
- 最后一招 Compiler:代码规范了再上 React Compiler,让它自动 memo。
动手实验
- 看三条件:在
beginWork的 bailout 检查(4113)打断点,更新父组件、子组件 props 引用不变,观察三个条件怎么逐条满足。 - 验证 memo 收益边界:给一个 props 永远是字面量的组件包 memo,在 Profiler 里对比,往往没收益(引用每次都一样,React 默认就 bailout 了)。
- 看"钻下去":父 bailout 但子 setState,打断点
bailoutOnAlreadyFinishedWork,观察它返回workInProgress.child(钻下去)而不是null(整棵跳过)。
小结
- 优化本质 = 让更多子树 bailout。
- bailout 三条件:props 没变 + 自身无更新 + 子树无活,缺一不可。
- bailout 分两层:完全跳过(return null)和"本组件省、往下钻"(return child)。
- memo 是手动保证条件一,只在 props 变引用不变时有收益。
- React Compiler 是构建期自动 memo,把第 10、15 章的优化提前到编译期。
- 下一步:如果渲染里抛异常了怎么办,第 25 章错误处理。