出品 | 51CTO技术栈(微信号:blog51cto)
React的一个改动细节引起了前端圈的声讨。
“这项改动违背了React的第一设计原则!”
“这会显著降低使用React框架的网站的性能。”
“这是一个令人恼火、无法理解的改动!”
提起React,作为世界上最受欢迎和使用最多的UI框架,许多网络“大牌”都有它的身影:不用提新潮一代的Netflix、Airbnb、Discord 等,仅诞生地Meta(Facebook、Instagram 和 Whatsapp)就已经足够让其扬名天下。
考虑到 React 用于创建数十亿人使用的用户界面,这样说也不为过:互联网流量的很大一部分是由 React “处理”的。
一、一场讨论让React官方暂停了项目
这里的主要话题,正在讨论的变化正是React19版本。
随着React 19全新功能和 DX 改进,处理并行fetching问题的方式发生了变化:禁用了同一 Suspense 边界内兄弟节点的并行渲染。这实质上为在这些兄弟节点内部获取的数据引入了数据获取瀑布。
这个小变化起初并没有引起人们注意,却被被一位细心的前端大牛Dominik发现了发到海外社交媒体上,并表示这可能会显著降低许多依赖 React 的网站的性能,一时间引起了网友们的鼎沸讨论。
一切都从Dominik发的这条推文开始:
图片
帖子中指出:React19查询现在是以瀑布形式运行。
现在,对于那些不认识 Dominik(又名TkDodo)的人来说,他与传奇的Tanner Linsley一起是广泛使用的TanStack Query的核心维护者之一。
一切都从这条推文开始:
图片
Dominik(又名TkDodo)何许人?他与传奇的Tanner Linsley一起是广泛使用的TanStack Query的核心维护者之一。
帖子中指出:React19在Suspense处理并行fetching的方式跟React18不一样了?查询现在是以瀑布形式运行。
随后引起了连锁反应:
1.“这是一个令人恼火,不可理解的改动!不仅客户端组件中也变动了,甚至RSC中的并行fetch也变了。这对react-query而言是一场灾难。”
图片
2.“可想而知,这也会影响lazily加载的组件(react.lazy)!”
图片
3.我能理解Dominik的痛苦:这难道不违背React第一设计原则:compositon吗?如果我理解正确的话,现在你无法再使用复杂的预取技巧来编写数据需求。或者你必须将所有数据提取提升到公共父级。
图片
此次React 19禁用了同一 Suspense 边界内兄弟节点的并行渲染,这实质上为在这些兄弟节点内部获取的数据引入了数据获取瀑布。
以下是此类事情的一个例子:
图片
https://github.com/facebook/react/pull/26380#issuecomment-2166178673
所有这一切中最糟糕的是,除了性能会有严重下降,而且还将影响很多依赖这种模式的人,但只有一行项目符号毫不客气地提到了这一变化。
很多人也许纳闷:究竟是怎么回事?下面我们就细细道来。
二、悬疑回顾
为了理解这到底是什么,我们首先需要快速回顾一下 React 的 Suspense。
Suspense 是一个 React 组件,它允许您显示一个回退,直到其子组件完成加载,要么是因为这些子组件是延迟加载的,要么是因为它们正在使用启用 Suspense 的数据获取机制。
使用方式如下:
<Suspense fallback={<Loading />}>
<ComponentThatFetchesDataOrIsLazyLoaded />
</Suspense>
尽管 Suspense 已经成为 React API 的一部分很长一段时间了,但是在很长一段时间内,它的唯一官方批准用途是使用来延迟加载组件React.lazy,这对于对应用程序进行代码分割并仅在需要时加载分割的部分非常有用。
当与 一起使用时,当第一次尝试渲染延迟加载的组件时(即,在延迟加载之前),它会触发 Suspense 边界(即包装组件)并渲染回退,直到获取组件的代码完成,然后它会渲染组件React.lazySuspense本身。
长期以来,我们一直被承诺在客户端上为 Suspense 提供官方数据提取支持(使用 RSC 时,它已经在服务器上运行),但直到现在我们才真正实现它,尽管如此,许多库(TanStack Query 就是其中之一)已经通过研究 React 的内部结构实现了它。正因为如此,目前生产中有许多应用程序确实使用Suspense在客户端上进行数据提取。
三、性能变慢了?
从现在起(React 18.3.1),当在同一个 Suspense 边界内使用启用 Suspense 的数据获取或延迟加载多个组件时,React 会在退出渲染之前尝试渲染所有兄弟组件,即使第一个兄弟组件已暂停。
实际上,这意味着这些兄弟节点中发生的数据获取或延迟加载将全部并行启动。
下面是一个展示这个想法的例子:
function App() {
return (
<>
<Suspense fallback={"Loading..."}>
<ComponentThatFetchesData val={1} />
<ComponentThatFetchesData val={2} />
<ComponentThatFetchesData val={3} />
</Suspense>
</>
);
}
const ComponentThatFetchesData = ({ val }) => {
const result = fetchSomethingSuspense(val);
return <div>{result}</div>;
};
图片
演示地址:https://stackblitz.com/edit/vitejs-vite-x3nv7r?file=src%2FApp.jsx
在这个例子中(在 React 18 中),即使fetchSomethingSuspense导致第一个ComponentThatFetchesData暂停,React 仍然会尝试渲染它的兄弟,这将触发每个兄弟的并行数据获取。
通过查看每次触发数据提取时记录的控制台,可以看出:所有数据获取几乎同时开始。
现在让我们看看当我们在 React 19(canary)中运行完全相同的代码时会发生什么:
图片
演示地址:https://stackblitz.com/edit/vitejs-vite-55rddj?file=src%2FApp.jsx
当我们再次查看控制台时,我们注意到现在有一个瀑布,因为每个数据提取仅在前一个数据完成后才启动。
发生这种情况是因为以下 PR:
图片
在该 PR 引入更改之后,React 不会尝试渲染同一 Suspense 边界内的所有兄弟组件,而是会在第一个挂起的组件上放弃,在这种情况下,您会首先尝试渲染第一个组件,然后它会挂起,然后只有在其数据获取完成并且您可以渲染它之后,您才会到达下一个兄弟组件,它将再次挂起,依此类推每个兄弟组件。
此外,这种新行为不仅会影响使用 Suspense 进行数据提取的使用,还会影响使用React.lazy,因为这是一种旧模式,所以得到了官方支持并且更加广泛。
图片
四、背后是什么鬼
这一变化背后的理由(在前面提到的 PR 中写明)是,在实际暂停之前尝试渲染所有兄弟节点元素消耗,并且会延迟显示回退。
此外,这一变化与React 团队自 React 18 之前引入 Suspense 以来一直在推动的“获取时渲染”方法相辅相成。
理想情况下,我们不应该在使用它的同一组件的渲染上启动数据提取,而应该将其提升并尽早开始提取数据。
虽然从性能角度来看这无疑是最好的方法,但它确实带来了显著的 DX 缺陷,因为无法共置组件及其数据要求。
这里不会深入讨论这个话题,因为已经有很多人讨论了它,甚至有一个专门为解决这个问题而创建的库,但这里推荐某条特定观点的推文:
这里的主要内容是,如果不使用编译器,就不可能同时拥有最佳的性能特性以及组件及其数据需求的搭配,而这正是 Relay 所做的。
五、结局还算圆满
幸运的是,这个故事有一个圆满的结局。在经历了大量公众反对、激烈讨论以及幕后争论之后,React 团队最终决定暂时推迟这一改变。
图片
这并不是社区第一次反对在不考虑 React 在 Meta 和 Vercel 之外的使用方式的情况下引入的更改。React 团队(尤其是 Vercel)推动将 RSC 成为使用 React 构建的基本组成部分就是一个例子。
很明显,React 维护者和社区对于 React 的未来前景的看法存在分歧。这些问题沟通会有哪些进展?后续还有待观察。