React18中的SuspenseSSR

备受期待的 React 18(现已进入测试阶段)即将问世,并带来了全新的 Suspense SSR 架构。要理解新的架构,必须熟悉一些基本概念,比如客户端渲染、服务器端渲染、加载、等等。我们在上一节关于水合的文章中已经解释了这些概念。在跳转到新架构之前,建议您先阅读一下。

SSR是如何工作的?

在SSR中,数据被获取,并在服务器上基于React组件生成HTML。然后将HTML发送给客户端。

SSR 的步骤如下:

  1. 服务器会为整个应用程序加载数据。
  2. HTML 是由服务器上的 React 组件生成的,用于整个应用程序,然后发送给客户端。
  3. 在客户端(浏览器)上,会加载整个应用程序的 JavaScript 代码。
  4. 然后将 JavaScript 逻辑与整个应用程序的服务器生成的 HTML 进行连接。这个过程称为“水化”。它使网站具有交互性。

图片取自 Shaundai 在 React Conf 2021上的演讲。

我们在每个步骤中都强调了整个应用程序。这是因为,在下一个步骤开始之前,每个步骤必须同时完成整个应用程序。如果应用程序的某些部分比其他部分运行速度慢,那么这种方法效率不高。

让我们考虑一下SSR WG讨论中提到的例子。在这个例子中,我们的应用程序有一个导航栏、一个侧边栏和一个右面板,其中包含帖子和评论。

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Comments />
  </RightPane>
</Layout>

“评论”部分是我们应用程序中最重要的部分,用户对此很感兴趣。但是,假设 组件涉及大量数据的昂贵API请求,并且包含大量的JavaScript逻辑。现在,让我们来看看这个应用程序中的SSR问题。

在 React 18 之前,SSR 存在哪些问题?

1. 先收集所有资料,然后再展示任何东西。

正如我们之前所看到的,我们需要在向用户展示任何内容之前获取所有数据。这意味着我们还需要获取大量数据的评论,这可能需要一定的时间。这种做法效率不高,因为用户在屏幕上看不到任何内容。

现在我们只剩下两种选择——

  1. 延迟从服务器发送HTML文件。
  2. 将HTML中的评论排除在外。这会在客户端上增加渲染评论的开销。

这两种选择都不太理想。

2. 先加载所有内容,然后再hydration

我们知道,在开始进行数据加载之前,所有JavaScript代码都需要加载。同样地,我们的 组件包含很多复杂的JavaScript逻辑,需要一定的加载时间。

尽管已经加载了导航栏、侧边栏和帖子的JS代码,但无法开始进行数据加载。

我们再次面临两种选择。

  1. 等到所有 JavaScript 代码加载完成后再进行加载。但这并非最佳方案。
  2. 使用代码分片处理评论,并单独加载它们。这意味着我们必须将评论从服务器端HTML中排除。否则,React将不知道如何处理这段HTML,并在加载时将其丢弃。

3. 在与任何事物互动之前,先给所有组件hydration

假设我们的 组件有一个昂贵的渲染逻辑,需要花费一些时间来添加事件处理程序。众所周知,加载过程仅进行一次。这意味着一旦开始加载,React 将一直运行,直到完成。因此,我们必须等待所有组件加载完成后才能与它们进行交互。

假设一个用户不小心点击了一个帖子。现在,他想回到主页。但由于正在进行数据刷新,应用程序被冻结了。因此,即使主页链接在导航栏中是可见的,用户也无法导航。

对用户来说真是浪费时间啊!

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特性:

  1. 在服务器上实时传输HTML:要加入其中,我们需要将原来的 renderToString 方法切换为新的 renderToPipeableStream 方法。
  2. 客户端选择性加载:要启用此功能,我们需要在客户端切换到 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中找到。



请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部