如何在 React 18中 利用Suspense 实现 服务端渲染(SSR)

前端Q

共 8673字,需浏览 18分钟

 · 2022-03-04

概述


React 18 将包括对 其服务器端渲染 (SSR) 性能的架构做了改进。这些改进带来了实质性的效果,是几年来其团队工作的结晶。大多数的改进点都是在幕后进行的,但您需要了解一些选择加入机制,尤其是您在不适用框架的情况下。
主要的新API是 pipeToNoWritable, 您可以在 "升级到服务器上的React18" 这篇文章中阅读了解。在最终的正式版本中,我们也将计划写更多关于它的细节。

如何在服务端使用React18

https://github.com/reactwg/react-18/discussions/22
现有的API是 ,此文主要是对新架构、其设计及背后要解决的问题进行阐述。

服务端渲染(SSR)介绍


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

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

  • 在服务器端,获取整个应用程序的数据。

  • 在服务器端,将整个应用程序呈现为 HTML 字符串并将其发送到客户端。

  • 在客户端,加载整个应用程序的JavaScript 代码。

  • 在客户端,将JavaScript 逻辑连接到整个应用程序的服务器生成的HTML(这就是“hydration”)。

关键部分是,在下一步开始之前,整个应用程序的每个步骤都必须立即完成。如果其中一个环节比其他部分慢,将会影响整体的加载时间。

React18中,您可以使用  将您的应用程序分解成更小的独立单元,每个模块都是独自异步加载,并不会影响其余部分。即便是应用程序中最慢的模块也不会拖累较快的模块。因此,用户也将更快地看到内容,并更快的开始与之交互。

这些改进点都是框架内部自动完成的,您无需为它们编写任何特殊的代码。

通过案例演示来介绍什么是SSR ?


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

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

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

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

出现空白页面对用户来说非常的不友好,这也是我们推荐使用 SSR的原因。SSR允许您将服务器上的React组件渲染为HTML字符串并将其发送给用户。HTML的交互性不是很强(除了简单内置Web交互,如链接和表单输入)。然而,它让用户在JavaScript仍在加载时可以看到一些内容:

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

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

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

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

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


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

React服务器组件

https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html


现有 SSR 存在哪些问题?


上述方法有效,但在很多方面表现不佳。

1. 您必须获取所有内容,然后才能显示任何内容【整个过程是 同步进行的】

今天 SSR 的一个问题是它不允许组件“等待数据”。使用当前的 API,当您呈现为 HTML 时,您必须为服务器上的组件准备好所有数据。这意味着您必须先收集服务器上的所有数据,然后才能开始向客户端发送任何HTML。这是相当低效的。


例如,假设您要呈现带有评论的帖子。尽早显示注释很重要,因此您希望将它们包含在服务器 HTML 输出中。但是您的数据库或 API 层很慢,这是您无法控制的。现在你必须做出一些艰难的选择。如果您将它们从服务器输出中排除,则在 JS 加载之前用户将不会看到它们。但是,如果您将它们包含在服务器输出中,则必须延迟发送其余的 HTML(例如,导航栏、侧边栏,甚至是帖子内容),直到评论加载完毕并且您可以呈现完整的树。这不是很友好。

作为旁注,一些数据获取解决方案反复尝试将树渲染为 HTML 并丢弃结果,直到数据得到解析,因为 React 没有提供更符合人体工程学的选项。我们希望提供一种不需要如此极端妥协的解决方案。

2. 给任何板块补水之前,您必须加载所有数据

在您的 JavaScript 代码加载后,您将告诉 React “水合” HTML 并使其具有交互性。React 将在渲染组件时“遍历”服务器生成的 HTML,并将事件处理程序附加到该 HTML。为此,浏览器中组件生成的树必须与服务器生成的树相匹配。否则 React 无法“匹配它们!” 这样做的一个非常不幸的后果是,您必须先为客户端上的所有组件加载 JavaScript,然后才能开始对它们中的任何一个进行补水。


例如,假设评论小部件包含很多复杂的交互逻辑,为其加载 JavaScript 需要一段时间。现在你必须再次做出艰难的选择。最好将服务器上的评论呈现为 HTML,以便尽早将它们显示给用户。但是因为今天只能一次完成补水,所以在您加载评论小部件的代码之前,您无法开始对导航栏、侧边栏和帖子内容进行补水!当然,您可以使用代码拆分并单独加载它,但是您必须从服务器 HTML 中排除注释。否则 React 将不知道如何处理这块 HTML(它的代码在哪里?)并在水化过程中将其删除。

3. 在与任何事物交互之前,您必须先补充所有水分

融合作用本身也存在类似的问题。今天,React 一次性完成树的水化。这意味着一旦它开始 hydrating(本质上是调用你的组件函数),React 不会停止,直到它为整个树完成此操作。因此,您必须等待所有组件都 “融合” 后才能与它们中的任何一个进行交互。


例如,假设评论小部件具有昂贵的渲染逻辑。它可能在您的计算机上运行得很快,但在运行所有这些逻辑的低端设备上并不便宜,甚至可能会锁定屏幕几秒钟。当然,理想情况下,我们根本不会在客户端上有这样的逻辑(服务器组件可以提供帮助)。但是对于某些逻辑来说,这是不可避免的,因为它决定了附加的事件处理程序应该做什么并且对于交互性至关重要。因此,一旦水化开始,用户就无法与导航栏、侧边栏或帖子内容进行交互,直到整个树被水化。对于导航,这尤其令人遗憾,因为用户可能希望完全离开此页面——但由于我们正忙于补充水分,我们将它们保留在他们不再关心的当前页面上。


我们如何解决这些问题呢 ?


这些问题之间有一个共同点。它们迫使你在早点做某事(但因为它阻止所有其他工作而损害用户体验)或晚做某事(但因为你浪费时间而损害用户体验)之间做出选择。

这是因为有一个过程:获取数据(服务器)→ 渲染到 HTML(服务器)→ 加载代码(客户端)→ 水合物(客户端)。在应用程序的前一阶段完成之前,这两个阶段都不能开始。这就是它效率低下的原因。我们的解决方案是将工作分开,以便我们可以为屏幕的一部分而不是整个应用程序执行每个阶段。

这不是一个新颖的想法:例如,

Marko[https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/] 是实现此模式版本的 JavaScript Web 框架之一。挑战在于如何使这样的模式适应 React 编程模型。花了一段时间才解决。我们在 2018 年为此目的引入了该组件。我们引入它时仅支持在客户端延迟加载代码。但目标是将其与服务器渲染集成并解决这些问题。


让我们看看如何在 React 18 中使用来解决这些问题。


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


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

  • 在服务器上流式传输 HTML。要选择使用它,您需要renderToString从新pipeToNodeWritable方法切换到新方法,如此处所述

  • 对客户进行选择性水合作用。要选择加入它,您需要在客户端上切换到createRoot,然后开始使用。(https://github.com/reactwg/react-18/discussions/5

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



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

使用现有的 SSR,渲染HTML和水化是“全有或全无”。首先渲染所有HTML:
<main>  <nav>        <a href="/">Homea>   nav>  <aside>        <a href="/profile">Profilea>  aside>  <article>        <p>Hello worldp>  article>  <section>        <p>First commentp>    <p>Second commentp>  section>main>
客户端最终将会展示为:

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

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

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

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

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

现在在最初的HTML中找不到注释:
<main>  <nav>        <a href="/">Homea>   nav>  <aside>        <a href="/profile">Profilea>  aside>  <article>        <p>Hello worldp>  article>  <section id="comments-spinner">        <img width=400 src="spinner.gif" alt="Loading..." />  section>main>

故事到这里还没有结束。当评论的数据在服务器上准备好时,React会将额外的 HTML 发送到同一个流中,以及一个最小的内联