服务器端渲染(在本文中缩写为“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。(或者至少,这就是我对自己解释这个术语的方式。)
水合之后,它是“像往常一样反应”:你的组件可以设置状态,响应点击等等:
你可以看到 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>