在 4月25日 React 发布了 19RC 版本(发布主版本前的最后一步),而根据 React 核心团队成员 Joe Savona 的描述,React 出现了问题因此停止了 19 的发布直到他们找到一个合理的修复方式:
那发生了什么呢?
在 React18 中,当 <Suspense>
包裹多个 children
时,当第一个组件抛出了 promise 时,React 会继续渲染这个组件的兄弟节点。而 React 团队认为这是非常影响效率的,因为当抛出了 promise 之后意味着一定会展示 fallback,此时渲染兄弟节点是浪费性能的,因此在 React19 中这个行为被改变了,会立即展示 fallback,等到 promise 被 resolve 之后才会继续渲染兄弟节点。
React19 的重大失误
让我们来看一个实际的例子吧。
比如我们现在在 <Suspense>
中包裹了多个 <Display>
组件,每个组件会发起请求拿到对应 id
的数据:
<Suspense fallback={<div>loading</div>}>
<Display id={1} />
<Display id={2} />
<Display id={3} />
<Display id={4} />
<Display id={5} />
<Display id={6} />
</Suspense>
然后我们来看一下 React18/19 的行为有什么不同:
React19
点击查看 Demo:codesandbox.io/p/sandbox/r…
对应 Waterfall:
React18
点击查看 Demo:codesandbox.io/p/sandbox/r…
可以看到更快的展示了内容,对应 Waterfall:
我们可以看到在 React19 时 Waterfall 是顺序的,也就是说后续的请求会在上一次请求完成后被发出,而我们看 React18 的 Waterfall 可以发现请求是同时被发出的。
也就是说从 React18 升级到 React19 之后,这意味着这会对你的应用造成性能问题。
我们可以看一下相关的改动 PR:github.com/facebook/re…
核心改动在 packages/react-reconciler/src/ReactFiberWorkLoop.js
其它大部分都是测试文件。
在原先当抛出错误之后会被 try-catch
捕获,然后在 handleThrow
中会更新 workInProgressSuspendedReason
值:
之后会走到 unwindSuspendedUnitOfWork
,这里其实就是正常的流程了,也就是以 DFS 的方式继续遍历兄弟节点,各个 Fiber Node 通过 child、return、sibling 来关联:
然后当回到 Suspense 节点,并且在这个 Fiber Node 上进行标记,代表说现在应该去渲染 Suspense 的fallback 了,这样在 React18 中抛出 promise 的兄弟节点也有机会得到渲染。而在上面的 PR 中我们可以看到当 React 捕获到错误后会从当前节点直接向上寻找 Suspense:
do {
// ...
const next = unwindWork(current, incompleteWork, renderLanes);
if (next !== null) { // 找到 Suspense 终止
workInProgress = next;
return;
}
const returnFiber = incompleteWork.return; // 往上找父节点
// ...
incompleteWork = returnFiber;
} while (incompleteWork !== null);
也就是说取消了渲染兄弟节点的过程,而是直接向上找,直到找到 Suspense,并从 Suspense 开始渲染。
如何解决
社区的一个建议是是否渲染兄弟节点还是直接渲染 Suspense fallback 应该是一个可选择的事情,比如我们可以向 Suspense 加入 strategy={"parallel" | "sequential"}
选项来动态的控制这种行为。
另外,在 RC 版本发布前 React 要经历很多阶段 experimental
-> cancary
-> beta
-> next
-> rc
-> latest
,而这个问题在 rc
之前并没有被社区发现,这使得 React 团队不得不思考如何让更多开发者使用起来从而获得更多反馈,而不仅仅为社区的一些框架比如 nextjs(狗头)服务。