React18中所应用的SSR(内置支持了 React.lazy 的 全新 SSR 架构)

1,429 阅读5分钟

服务器端渲染(在本文中缩写为“SSR”)让您可以从服务器上的 React 组件生成 HTML,并将该 HTML 发送给您的用户。SSR 允许您的用户在您的 JavaScript 包加载和运行之前查看页面的内容。

React 中的 SSR 总是发生在几个步骤中:

  • 在服务器上,获取整个应用程序的数据。
  • 然后,在服务器上,将整个应用程序呈现为 HTML 并将其发送到响应中。
  • 然后,在客户端上,加载整个应用程序的 JavaScript 代码。
  • 然后,在客户端,将 JavaScript 逻辑连接到整个应用程序的服务器生成的 HTML(这就是“hydration”)。

关键部分是,在下一步可以开始之前,整个应用程序的每个步骤都必须立即完成。 如果您的应用程序的某些部分比其他部分慢,这将效率低下,几乎每个非平凡的应用程序都是如此。

反应18,您可以使用<Suspense>您的应用程序分解成更小的独立的单元,将独立完成这些步骤彼此并不会阻止应用程序的其余部分。因此,您的应用程序的用户将更快地看到内容并能够更快地开始与之交互。应用程序中最慢的部分不会拖下速度快的部分。这些改进是自动的,您无需为它们编写任何特殊的协调代码即可工作。

什么是 SSR?

当用户加载您的应用程序时,您希望尽快显示一个完全交互式的页面:

此插图使用绿色表示页面的这些部分是交互式的。换句话说,它们所有的 JavaScript 事件处理程序都已附加,单击按钮可以更新状态,等等。

但是,页面在 JavaScript 代码完全加载之前无法进行交互。这包括 React 本身和您的应用程序代码。对于非平凡的应用程序,大部分加载时间将用于下载您的应用程序代码。

如果您不使用 SSR,则用户在 JavaScript 加载时只会看到一个空白页面:

这不是很好,这就是我们推荐使用 SSR 的原因。SSR 允许您将服务器上的React 组件渲染为 HTML 并将其发送给用户。HTML 的交互性不是很强(除了简单的内置 Web 交互,如链接和表单输入)。然而,它让用户在 JavaScript 仍在加载时看到一些东西

此处,灰色说明屏幕的这些部分尚未完全交互。您的应用程序的 JavaScript 代码尚未加载,因此单击按钮不会执行任何操作。但特别是对于内容较多的网站,SSR 非常有用,因为它可以让连接较差的用户在 JavaScript 加载时开始阅读或查看内容。

当 React 和你的应用程序代码都加载时,你想让这个 HTML 交互。你告诉 React:“这是App在服务器上生成这个 HTML的组件。将事件处理程序附加到该 HTML!” React 将在内存中渲染你的组件树,但它不会为它生成 DOM 节点,而是将所有逻辑附加到现有的 HTML。

渲染组件和附加事件处理程序的过程称为“水化”。 这就像用交互性和事件处理程序的“水”浇灌“干”的 HTML。(或者至少,这就是我对自己解释这个术语的方式。)

水合之后,它是“像往常一样反应”:你的组件可以设置状态,响应点击等等:

image.png

你可以看到 SSR 是一种“魔术”。它不会使您的应用程序完全交互更快。相反,它可以让您更快地显示应用程序的非交互式版本,以便用户可以在等待 JS 加载时查看静态内容。然而,这个技巧对网络连接较差的人产生了巨大的影响,并提高了整体感知性能。由于更容易的索引和更快的速度,它还可以帮助您进行搜索引擎排名。

注意:不要将 SSR 与服务器组件混淆。服务器组件是一个更具实验性的功能,仍在研究中,可能不会成为最初的 React 18 版本的一部分。您可以在此处了解服务器组件。服务器组件是对 SSR 的补充,并将成为推荐的数据获取方法的一部分,但本文与它们无关。

React 18:流式 HTML 和选择性水化

Suspense 解锁的 React 18 中有两个主要的 SSR 特性:

  • ****在服务器上流式传输 HTML。要选择加入,您需要renderToString从新pipeToNodeWritable方法切换到新方法,如此处所述
  • ****对客户进行选择性水合作用。要选择加入它,您需要在客户端上切换到createRoot,然后开始使用<Suspense>.

要了解这些功能的作用以及它们如何解决上述问题,让我们返回到我们的示例。

在获取所有数据之前流式传输 HTML

使用今天的 SSR,渲染 HTML 和水化是“全有或全无”。首先渲染所有 HTML:

  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>

客户端最终收到它:

然后加载所有代码并为整个应用程序注入水分:

但是 React 18 给了你一个新的可能性。您可以用 包裹页面的一部分<Suspense>

例如,让我们包装注释块并告诉 React,在它准备好之前,React 应该显示该<Spinner />组件:

  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

客户端最终收到它:

然后加载所有代码并为整个应用程序注入水分:

但是 React 18 给了你一个新的可能性。您可以用 包裹页面的一部分<Suspense>

例如,让我们包装注释块并告诉 React,在它准备好之前,React 应该显示该<Spinner />组件:

  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

通过包装<Comments><Suspense>,我们告诉 React它不需要等待评论开始为页面的其余部分流式传输 HTML。 相反,React 将发送占位符(一个微调器)而不是评论:

现在在最初的 HTML 中找不到注释:

  <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 放在“正确的位置”:

  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

转自 github.com/reactwg/rea…