React 18 中新的 Suspense SSR 架构

概述

React 18 将包括对 React 服务器端渲染(SSR)性能的架构改进。这些改进是实质性的,并且是几年来工作的结晶。这些改进大多是在幕后进行的,但有一些选择性机制你需要注意,特别是如果你不使用框架的话。

主要的新 API 是  pipeToNodeWritable,你可以在 Upgrading to React 18 on the Server 中了解到。我们计划在细节上做更多的实现,因为这不是最终版本,并且还有一些事情需要解决。

现有的主要的 API 是  .

本文是对新的架构以及它的设计和所解决的问题的简单概述。

简而言之

服务器端渲染(在这篇文章中缩写为 “SSR”)让你能在服务器上将 React 组件生成 HTML,并将该 HTML 发送给你的用户。SSR 能让你的用户在你的 JavaScript 包加载和运行之前看到页面的内容。

React 中的 SSR 总是分几个步骤进行:

  • 在服务器上获取整个应用的数据。
  • 然后在服务器上将整个应用程序渲染成 HTML 并在响应中返回。
  • 然后在客户端加载整个应用程序的 JavaScript 代码。
  • 然后在客户端将 JavaScript 逻辑绑定到服务器为整个应用程序生成的 HTML(这个过程叫 “hydration”)。

关键在于,每一步都必须在下一步开始之前一次性完成整个应用程序的工作。如果你的应用程序的某些部分比其他部分慢,这样做的效率不高。这也是几乎所有具有一定规模的应用面临的问题。

React 18 让你使用    来将你的应用程序分解成较小的独立单元。这些单元将独立完成这些步骤,并且不会阻碍应用程序的其他部分。因此,你的应用程序的用户将更快地看到内容,并能更快地开始与应用程序交互。你的应用程序中最慢的部分不会拖累那些较快的部分。这些优化是自动的。你不需要写任何特殊的代码来实现这个功能。

这也意味着 React.lazy 现在可以和 SSR 一起 “正常工作”。这里有一个 demo.

(如果你不使用框架,你将需要改变 HTML 生成的具体方式 wired up。)

什么是 SSR?

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

React 18 中新的 Suspense SSR 架构_第1张图片

这幅插图用绿色来表达页面的可交互的部分。换句话说,它们所有的 JavaScript 事件处理程序都已经绑定好了,点击按钮可以更新状态等等。

然而,在页面的 JavaScript 代码完全加载之前,该页面是不能交互的。这包括 React 本身和你的应用程序代码。对于具有一定规模的应用程序,大部分的加载时间将用于下载你的应用程序代码。

如果你不使用 SSR,用户在 JavaScript 加载时看到的唯一东西就是一个空白的页面。

React 18 中新的 Suspense SSR 架构_第2张图片

这不是很好,这就是为什么我们建议使用 SSR。SSR 让你在服务器上把你的 React 组件渲染成 HTML 并发送给用户。HTML 的交互性不强(除了简单的内置网络交互,如链接和表单输入)。但是,它能让用户在 JavaScript 仍在加载时看到一些东西

React 18 中新的 Suspense SSR 架构_第3张图片

这里,屏幕中灰色部分代表还没有完全可交互的部分。你的应用程序的 JavaScript 代码还没有加载完成,所以点击按钮是没有任何响应的。但特别是对于内容繁杂的网站,SSR 非常有用,因为它可以让网络连接较差的用户在 JavaScript 加载时开始阅读或查看内容。

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

这个渲染组件和绑定事件处理程序的过程被称为 “hydration”。(这就像是用事件处理程序当作 “水” 来浇灌 “干燥” 的 HTML。至少,我是这样向自己解释这个术语的。)

hydration 之后,就是 “React 正常操作”:你的组件可以设置状态,响应点击等等:

React 18 中新的 Suspense SSR 架构_第4张图片

你可以看到 SSR 有点像 “魔术”。它不能使你的应用程序更快地完全可交互。相反,它让你更快地展示你的应用程序的非交互式版本,以便用户在等待 JS 加载时可以查看静态内容。然而,这一招对于网络连接不畅的人来说有很大的不同,而且提高了整体的感知性能。它还有助于你的搜索引擎排名,既是因为有更容易的索引,也是因为有更快的响应速度。

注意: 不要将 SSR 与服务器组件混淆。服务器组件是一个更具实验性的功能,目前仍在研究中,并且可能不会成为 React 18 最初版本的一部分。你从这里可以了解服务器组件。服务器组件是对 SSR 的补充,并将成为数据获取的推荐方式之一,但这篇文章并不介绍它们。

今天 SSR 有哪些问题?

上述方法是可行的,但在许多方面,它并不是最佳的。

在展示任何东西之前,必须先获取所有东西

如今 SSR 的一个问题是,它不允许组件 “等待数据”。在目前的 API 中,当你渲染到 HTML 时,你必须已经在服务器上为你的组件准备好所有的数据。这意味着你必须在服务器上收集所有的数据,然后才能开始向客户端发送任何 HTML。这样是很低效的。

例如,假设你想渲染一个带有评论的帖子。尽早显示评论是很重要的,所以你要在服务器的 HTML 输出中包括它们。但你的数据库或 API 层很慢,这是你无法控制的。现在,你必须做出一些艰难的选择。如果你把它们从服务器输出中排除,在 JS 加载完毕之前,用户就不会看到它们。但如果你把它们包含在服务器输出中,你就必须推迟发送其余的 HTML(例如,导航栏、侧边栏,甚至是文章内容),直到评论加载完毕,你才能渲染完整的组件树。这样并不好。

顺便提一下,一些数据获取方案会反复尝试将树渲染成 HTML 并丢弃结果,直到数据被解决。因为 React 没有提供更符合人体工程学的选项。我们想提供一个不需要如此极端妥协的解决方案。

你必须先装好所有的东西,然后才能对任何东西进行 hydration

在你的 JavaScript 代码加载后,你会告诉 React 将 HTML “hydrate” 并使其具有交互性。 React 在渲染你的组件时将 “走” 过服务器生成的 HTML,并将事件处理程序绑定到该 HTML 上。为了使其发挥作用,你的组件在浏览器中生成的树必须与服务器生成的树相匹配。否则 React 就不能 “匹配它们!” 这样做的一个非常不幸的后果是,你必须在客户端加载所有组件的 JavaScript,才能开始对任何组件进行 hydration

例如,假设评论小组件包含很多复杂的交互逻辑,并且需要花费一些时间为其加载 JavaScript。 现在你不得不再次做出艰难的选择。把服务器上的评论渲染成 HTML,以便尽早显示给用户,这是一个好办法。但是,由于如今的 hydration 只能一次完成,所以在加载评论小组件的代码之前,你不能开始 hydrate 导航栏、侧边栏和文章内容。当然,你可以使用代码分割并单独加载,但你必须将注释从服务器 HTML 中排除。否则 React 将不知道如何处理这块 HTML(它的代码在哪里?),并在 hydration 过程中删除它。

在与任何东西互动之前,你必须 hydrate 所有的东西

hydration 本身也有一个类似的问题。如今,React 一次性完成树的 hydration。这意味着,一旦它开始 hydrate(本质上是调用你的组件函数),React 就不会停止 hydration 的过程,直到它为整个树完成 hydration。因此,你必须等待所有的组件被 hydrated,才能与任何组件进行交互。

例如,我们说评论小组件有昂贵的渲染逻辑。它在你的电脑上可能运行得很快,但在低端设备上运行这些逻辑的成本并不低,甚至可能使得屏幕被锁定好几秒钟。当然,在理想情况下,我们在客户端不会这样的逻辑(这是服务器组件可以帮助解决的问题)。但对于某些逻辑来说,这是不可避免的。这是因为它决定了所附的事件处理程序应该做什么,而且对于交互性是至关重要的。因此,一旦开始 hydration,用户就不能与导航栏、侧边栏或文章内容互动,直到整棵树完成 hydration。对于导航来说,这是特别不幸的,因为用户可能想完全离开这个页面,但由于我们正忙于 hydration,我们把他们留在他们不再关心的当前页面上。

我们如何解决这些问题?

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

这是因为有一个 “瀑布”(流程):获取数据(服务器)→ 渲染成 HTML(服务器)→ 加载代码(客户端)→ hydration(客户端)。任何一个阶段都不能在前一个阶段结束之前开始。 这就是为什么它的效率很低。我们的解决方案是将工作分开,这样我们就可以为屏幕的一部分而不是整个应用程序做这些阶段的工作。

这并不是一个新奇的想法:比如说:Marko 是实现该模式的一个 JavaScript 网络框架。将这样的模式适应于 React 编程模型具有一定的挑战性。我们也因此花了一段时间来解决这个难题。我们在 2018 年为此目的引入了 组件。当我们引入它时,我们只支持它在客户端进行惰性加载代码。但我们的目标是将它与服务器渲染结合起来,解决这些问题。

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

React 18:流式 HTML 和选择性 hydration

在 React 18 中,有两个主要的 SSR 功能是由 Suspense 解锁的。

  • 在服务器上流式传输 HTML。要使用这个功能,你需要从 renderToString 切换到新的 pipeToNodeWritable 方法,如此处描述
  • 在客户端进行选择性的 hydration。要使用这个功能,你需要在客户端 切换到createRoot,然后开始用 包装你的应用程序的一部分。

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

在所有数据被获取之前,使用流式 HTML

如今的 SSR 中,渲染 HTML 和 hydration 是 “全有或全无” 的。首先,你要渲染所有的 HTML:

Hello world

First comment

Second comment

客户端最终会收到它:

React 18 中新的 Suspense SSR 架构_第5张图片

然后你加载所有的代码,并对整个应用程序进行 hydration:

React 18 中新的 Suspense SSR 架构_第6张图片

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

例如,让我们包裹评论块并告诉 React,在它准备好之前,React 应该显示 组件。


  
  
  
    
    }>
      
    
  

通过将 包装成 ,我们告诉 React,它不需要等待评论就可以开始为页面的其他部分传输 HTML。相反,React 将发送占位符(一个旋转器)而不是评论:

React 18 中新的 Suspense SSR 架构_第7张图片

现在在最初的 HTML 中找不到评论了:

Hello world

Loading...

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

因此,甚至在 React 本身加载到客户端之前,迟来的评论的 HTML 就会 “弹出”。

React 18 中新的 Suspense SSR 架构_第8张图片

这就解决了我们的第一个问题。现在你不必在显示任何东西之前获取所有的数据了。如果屏幕的某些部分延迟了最初的 HTML,你就不必在延迟所有的 HTML 或将其排除在 HTML 之外之间做出选择。你可以只允许那部分内容在 HTML 流中稍后 “涌入”。

不同于传统的流式 HTML,它不一定要按照自上而下的顺序发生。例如,如果侧边栏需要一些数据,你可以用 Suspense 包装它,React 将会发出一个占位符,然后继续渲染帖子。然后,当侧边栏的 HTML 准备好了,React 会把它和

你可能感兴趣的:(React 18 中新的 Suspense SSR 架构)