如何在 React 18中 利用Suspense 实现 服务端渲染(SSR)
如何在服务端使用React18
https://github.com/reactwg/react-18/discussions/22
服务器端渲染(在本文中缩写为“SSR”)让您可以从服务器上的 React 组件生成 HTML,并将该 HTML 发送给您的用户。SSR 允许您的用户在您的 JavaScript 包加载和运行之前查看页面的内容。
React 中的 SSR 总是发生在几个步骤中:
在服务器端,获取整个应用程序的数据。
在服务器端,将整个应用程序呈现为 HTML 字符串并将其发送到客户端。
在客户端,加载整个应用程序的JavaScript 代码。
在客户端,将JavaScript 逻辑连接到整个应用程序的服务器生成的HTML(这就是“hydration”)。
关键部分是,在下一步开始之前,整个应用程序的每个步骤都必须立即完成。如果其中一个环节比其他部分慢,将会影响整体的加载时间。
React18中,您可以使用 <Suspense> 将您的应用程序分解成更小的独立单元,每个模块都是独自异步加载,并不会影响其余部分。即便是应用程序中最慢的模块也不会拖累较快的模块。因此,用户也将更快地看到内容,并更快的开始与之交互。
此插图使用绿色表示页面的这些部分是交互式的。换句话说,它们所有的 JavaScript 事件处理程序都已附加,单击按钮可以更新状态,等等。
但是,页面在 JavaScript 代码完全加载之前无法进行交互。这包括 React 本身和您的应用程序代码。对于非React的应用程序,大部分加载时间将用于下载您的应用程序代码。
如果您不使用 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
上述方法有效,但在很多方面表现不佳。
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 编程模型。花了一段时间才解决。我们<Suspense>在 2018 年为此目的引入了该组件。我们引入它时仅支持在客户端延迟加载代码。但目标是将其与服务器渲染集成并解决这些问题。
让我们看看如何<Suspense>在 React 18 中使用来解决这些问题。
Suspense 解锁的 React 18 中有两个主要的 SSR 特性:
在服务器上流式传输 HTML。要选择使用它,您需要renderToString从新pipeToNodeWritable方法切换到新方法,如此处所述。
对客户进行选择性水合作用。要选择加入它,您需要在客户端上切换到createRoot,然后开始使用<Suspense>。(https://github.com/reactwg/react-18/discussions/5)
要了解这些功能的作用以及它们如何解决上述问题,让我们返回到我们的示例。
<main>
<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,在它准备好之前,React 应该显示该<Spinner />组件:
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
将<Comments>包装成<Suspense>,我们告诉 React它不需要等待评论开始为页面的其余部分流式传输 HTML。相反,React 将发送占位符(一个微调器)而不是评论:
<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 放在“正确的位置”:
<div hidden id="comments">
<!-- 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>
结果,即使在 React 本身加载到客户端之前,迟来的 HTML 评论也会“弹出”:
这就解决了我们的第一个问题。现在,您不必先获取所有数据,然后才能显示任何内容。如果屏幕的某些部分延迟了初始 HTML,则您不必在延迟所有HTML 或将其从 HTML 中排除之间做出选择。您可以只允许该部分稍后在 HTML 流中“弹出”。
与传统的 HTML 流不同,它不必按自上而下的顺序发生。例如,如果侧边栏需要一些数据,您可以将其包装在 Suspense 中,React 会发出一个占位符并继续渲染帖子。然后,当侧边栏 HTML 准备好时,React 会将其与将<script>其插入正确位置的标签一起流式传输——即使帖子的 HTML(在树中更远的位置)已经发送!不要求以任何特定顺序加载数据。您指定微调器应该出现的位置,React 会找出其余的。
注意:为此,您的数据获取解决方案需要与 Suspense 集成。服务器组件将开箱即用地与 Suspense 集成,但我们还将提供一种方法让独立的 React 数据获取库与之集成。
在所有代码加载之前对页面进行“水分”补充
我们可以更早地发送初始 HTML,但我们仍然有问题。在评论小部件的 JavaScript 代码加载之前,我们无法开始在客户端上对我们的应用程序进行补水。如果代码很大,这可能需要一段时间。
为了避免较大体积的组件包,你通常会使用“代码拆分”:你会指定一段代码不需要同步加载,你的捆绑器会将它拆分成一个单独的<script>标签。
您可以使用代码拆分React.lazy从主包中拆分注释代码:
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
以前,这不适用于服务器渲染。(据我们所知,即使是流行的解决方法也迫使您在选择退出代码拆分组件的 SSR 或在所有代码加载后对其进行补充之间做出选择,这在某种程度上违背了代码拆分的目的。)
但是在 React 18 中,<Suspense>可以让您在评论小部件加载之前对应用程序 进行补水。
从用户的角度来看,最初他们会看到以 HTML 形式流入的非交互式内容:
然后您告诉React去“水合”,评论的代码还没有,但没有关系:
这是选择性水合作用的一个例子。通过包装Comments中<Suspense>,你告诉阵营,他们不应该阻止页面的其余部分流和,事实证明,水化,太!这意味着第二个问题解决了:您不再需要等待所有代码加载才能开始补水。React 可以在加载部件时对其进行水合。
React 将在其代码加载完成后开始为评论部分补水:
多亏了 Selective Hydration,大量的 JS 不会阻止页面的其余部分变得可交互。
在流式传输所有代码之前对页面进行“水分”补充
React 会自动处理所有这些,因此您无需担心事情会以意外的顺序发生。例如,即使 HTML 正在流式传输,它也可能需要一段时间才能加载:
如果 JavaScript 代码早于所有 HTML 加载,React 没有理由等待!它将滋润页面的其余部分:
当注释的 HTML 加载时,它将显示为非交互式,因为 JS 还没有:
当我们将评论包裹在<Suspense>. 现在它们的水分不再阻止浏览器做其他工作。
例如,假设用户在添加评论时单击侧边栏:
在 React 18 中,Suspense 边界内的水化内容发生在浏览器可以处理事件的微小间隙中。多亏了这一点,点击会立即处理,并且在低端设备上长时间水合期间,浏览器不会出现卡住现象。例如,这让用户可以离开他们不再感兴趣的页面。
在我们的例子中,只有评论被包裹在 Suspense 中,所以页面的其余部分在一次传递中发生。但是,我们可以通过在更多地方使用 Suspense 来解决这个问题!例如,让我们也包装侧边栏:
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
现在两个人可以从服务器包含导航栏和后最初的HTML之后流。但这也会影响水合作用。假设它们两个的 HTML 都已加载,但它们的代码尚未加载:
然后,包含侧边栏和注释代码的包加载。React 将尝试将它们都水化,从它在树中较早找到的 Suspense 边界开始(在本例中,它是侧边栏):
但是假设用户开始与评论小部件交互,为此还加载了代码:
React 会记录发生的点击,并优先处理评论,因为它更紧急:
在评论“水合”之后,React“重放”记录的点击事件(通过再次调度它)并让您的组件响应交互。然后,既然 React 无事可做,React 将“水化” 侧边栏:
这就解决了我们的第三个问题。多亏了选择性水合作用,我们不必“为了与任何东西互动而将所有东西都水化”。React 会尽早开始为所有内容补水,并根据用户交互优先考虑屏幕上最紧急的部分。如果您考虑到在整个应用程序中采用 Suspense 时,边界将变得更加细化,则选择性水化的好处将变得更加明显:
在此示例中,用户在水合开始时单击第一条评论。React 将优先处理所有父 Suspense 边界的内容,但会跳过任何不相关的兄弟姐妹。这会产生一种错觉,即水合是即时的,因为交互路径上的组件首先被水合。React 将立即为应用程序的其余部分补水。
在实践中,您可能会在应用程序的根目录附近添加 Suspense:
<Layout>
<NavBar />
<Suspense fallback={<BigSpinner />}>
<Suspense fallback={<SidebarGlimmer />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</RightPane>
</Suspense>
</Layout>
在此示例中,初始 HTML 可以包含<NavBar>内容,但其余部分将在加载相关代码后立即流入并混合部分,优先考虑用户与之交互的部分。
注意:您可能想知道您的应用程序如何在这种非完全水合状态下工作。设计中有一些微妙的细节使其发挥作用。例如,不是单独对每个单独的组件进行水合,而是对整个<Suspense>边界进行水合。由于<Suspense>已用于不会立即出现的内容,因此您的代码对其子项不立即可用具有弹性。React 总是按照父级优先顺序进行 hydration,因此组件总是设置了它们的 props。React 推迟调度事件,直到从事件点开始的整个父节点都被水合。最后,如果父级更新导致尚未水合的 HTML 变得陈旧,React 将隐藏它并将其替换为fallback您指定直到代码加载完毕。这确保了树对用户来说是一致的。你不需要考虑它,但这就是让它起作用的原因。
我们准备了一个演示,您可以尝试了解新的 Suspense SSR 架构如何工作。它被人为地减慢,因此您可以调整延迟server/delays.js:
API_DELAY 允许您在服务器上获取更长的注释,展示如何尽早发送 HTML 的其余部分。
JS_BUNDLE_DELAY让你延迟<script>标签加载,展示评论小部件的 HTML 如何在 React 和你的应用程序包下载之前“弹出”。
ABORT_DELAY 如果在服务器上获取时间太长,您可以看到服务器“放弃”并将渲染交给客户端。
https://codesandbox.io/s/github/facebook/react/tree/master/fixtures/ssr2?file=/src/App.js
React 18 为 SSR 提供了两个主要特性:
流式 HTML允许您尽早开始输出 HTML,将附加内容的 HTML 与<script>将它们放在正确位置的标签一起流式传输。
Selective Hydration可让您在剩余的 HTML 和 JavaScript 代码完全下载之前尽早开始对应用程序进行补充。它还优先考虑为用户与之交互的部分补水,从而产生即时补水的错觉。
这些特性解决了 React 中 SSR 的三个长期存在的问题:
在发送 HTML 之前,您不再需要等待所有数据加载到服务器上。相反,当您有足够的内容显示应用程序的外壳时,您立即开始发送 HTML,并在准备好时流式传输其余的 HTML。
您不再需要等待所有 JavaScript 加载完毕才能开始补水。相反,您可以将代码拆分与服务器渲染一起使用。服务器 HTML 将被保留,当相关代码加载时,React 将对其进行水合。
您不再需要等待所有组件都开始与页面交互。相反,您可以依靠 Selective Hydration 来确定用户与之交互的组件的优先级,并尽早对它们进行交互。
该<Suspense>组件可作为所有这些功能的选择。改进本身在 React 内部是自动的,我们希望它们能够与大多数现有的 React 代码一起使用。这展示了以声明方式表达加载状态的能力。从if (isLoading)到看起来可能没有很大的变化<Suspense>,但它是解锁所有这些改进的原因。
以上译文,避免不了措辞不当之处,还请谅解,如需查看原文请访问如下链接:
https://github.com/reactwg/react-18/discussions/37
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 趣谈前端 收货大厂一手好文章~
点个在看你最好看