React18中的SuspenseSSR
备受期待的 React 18(现已进入测试阶段)即将问世,并带来了全新的 Suspense SSR 架构。要理解新的架构,必须熟悉一些基本概念,比如客户端渲染、服务器端渲染、加载、等等。我们在上一节关于水合的文章中已经解释了这些概念。在跳转到新架构之前,建议您先阅读一下。
SSR是如何工作的?
在SSR中,数据被获取,并在服务器上基于React组件生成HTML。然后将HTML发送给客户端。
SSR 的步骤如下:
- 服务器会为整个应用程序加载数据。
- HTML 是由服务器上的 React 组件生成的,用于整个应用程序,然后发送给客户端。
- 在客户端(浏览器)上,会加载整个应用程序的 JavaScript 代码。
- 然后将 JavaScript 逻辑与整个应用程序的服务器生成的 HTML 进行连接。这个过程称为“水化”。它使网站具有交互性。
图片取自 Shaundai 在 React Conf 2021上的演讲。
我们在每个步骤中都强调了整个应用程序。这是因为,在下一个步骤开始之前,每个步骤必须同时完成整个应用程序。如果应用程序的某些部分比其他部分运行速度慢,那么这种方法效率不高。
让我们考虑一下SSR WG讨论中提到的例子。在这个例子中,我们的应用程序有一个导航栏、一个侧边栏和一个右面板,其中包含帖子和评论。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Comments />
</RightPane>
</Layout>
“评论”部分是我们应用程序中最重要的部分,用户对此很感兴趣。但是,假设
在 React 18 之前,SSR 存在哪些问题?
1. 先收集所有资料,然后再展示任何东西。
正如我们之前所看到的,我们需要在向用户展示任何内容之前获取所有数据。这意味着我们还需要获取大量数据的评论,这可能需要一定的时间。这种做法效率不高,因为用户在屏幕上看不到任何内容。
现在我们只剩下两种选择——
- 延迟从服务器发送HTML文件。
- 将HTML中的评论排除在外。这会在客户端上增加渲染评论的开销。
这两种选择都不太理想。
2. 先加载所有内容,然后再hydration
我们知道,在开始进行数据加载之前,所有JavaScript代码都需要加载。同样地,我们的
尽管已经加载了导航栏、侧边栏和帖子的JS代码,但无法开始进行数据加载。
我们再次面临两种选择。
- 等到所有 JavaScript 代码加载完成后再进行加载。但这并非最佳方案。
- 使用代码分片处理评论,并单独加载它们。这意味着我们必须将评论从服务器端HTML中排除。否则,React将不知道如何处理这段HTML,并在加载时将其丢弃。
3. 在与任何事物互动之前,先给所有组件hydration
假设我们的
假设一个用户不小心点击了一个帖子。现在,他想回到主页。但由于正在进行数据刷新,应用程序被冻结了。因此,即使主页链接在导航栏中是可见的,用户也无法导航。
对用户来说真是浪费时间啊!
Solution 解决方案
多亏了 React 18 中的新型 Suspense SSR 架构,为我们提供了所有问题的解决方案!
我们打破了传统的瀑布式工作模式,
Fetch data (server) → Render to HTML (server) → Load JS code (client) → Hydrate (client)
它可以让我们跟踪应用程序中某个部分屏幕的每个阶段,而不是整个应用程序。我们来详细了解一下这个问题。
React 18 中的流式 HTML 和选择性加载
Suspense是一种让我们“等待”某些代码加载的功能,同时在等待代码加载完成的过程中指定加载器。
React 18中由Suspense解锁了两个主要的SSR特性:
- 在服务器上实时传输HTML:要加入其中,我们需要将原来的 renderToString 方法切换为新的 renderToPipeableStream 方法。
- 客户端选择性加载:要启用此功能,我们需要在客户端切换到 createRoot ,然后开始将应用程序的部分代码包裹在
中。
继续刚才的例子,我们知道 <Comments>
组件是一个问题制造者。所以,让我们把它包裹在 <Suspense>
中,并告诉 React,除非它准备好了,否则 React 应该显示 <Spinner />
组件:
在所有数据被加载之前,先加载HTML内容。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
这样的话,我们就可以告诉React不要等待评论,而是开始为应用程序的其余部分流式传输HTML。评论将被Spinner占位符替换。
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
当服务器准备好评论数据后,React会将额外的HTML发送到同一流中,并使用 script 标签将HTML放置在“正确的位置”。
在React本身在客户端加载之前,评论的HTML就已经加载完毕了。这太酷了!
这被称为“流式HTML”。 这就是我们之前讨论的第一个问题的解决方法—— 在显示任何内容之前加载所有内容。
在所有代码加载完成之前对页面进行加载
通过将Comments
包裹在<Suspense>
标签中,我们不仅告诉React解锁页面其余部分的流式传输,而且还解锁了其加载过程!
这被称为“选择性加载”。 多亏了“选择性加载”,即使页面上有大量JavaScript代码,也不会影响页面的交互性。
在下面的图中,我们可以看到,b0
组件允许我们在 b1
组件加载之前对应用程序进行初始化。
在加载JS代码之后,React会开始对“评论”部分进行“水化”处理。这样一来,我们的第二个问题也解决了——先加载所有内容,然后再hydration
在所有HTML内容被加载之前对页面进行加载
将评论包裹在
当加载完用于评论的JavaScript代码后,页面将完全实现交互功能。
优先考虑Hydration
假设我们有多个组件被包裹在
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
React 会尝试对它们进行“水化”处理,首先从树中较早找到的 Suspense 边界(本例中的 SideBar)开始。
假设用户开始与评论部分进行交互,此时也会加载代码。在这种情况下,React会优先加载评论部分,因为认为其更为紧急,从而使评论部分具有交互性。之后,它将继续加载侧边栏。
这解决了我们的第三个问题——在与任何事物互动之前,先给所有组件hydration
这些隐藏在引擎中的改进解决了许多SSR问题。感谢React团队在Suspense上所做的大量工作!关于这些更改的更多信息可以在WG discussion中找到。