- 原文地址:New Suspense SSR Architecture in React 18
- 原文做者:
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:NieZhuZhu(弹铁蛋同窗)
- 校对者:Kimberly、Zavier
React 18 将包括对 React 服务器端渲染(SSR)性能的架构改进。这些改进是实质性的,而且是几年来工做的结晶。这些改进大可能是在幕后进行的,但有一些选择性机制你须要注意,特别是若是你不使用框架的话。html
主要的新 API 是 pipeToNodeWritable
,你能够在 Upgrading to React 18 on the Server 中了解到。咱们计划在细节上作更多的实现,由于这不是最终版本,而且还有一些事情须要解决。前端
现有的主要的 API 是 <Suspense>
.react
本文是对新的架构以及它的设计和所解决的问题的简单概述。android
服务器端渲染(在这篇文章中缩写为 “SSR”)让你能在服务器上将 React 组件生成 HTML,并将该 HTML 发送给你的用户。SSR 能让你的用户在你的 JavaScript 包加载和运行以前看到页面的内容。ios
React 中的 SSR 老是分几个步骤进行:git
关键在于,每一步都必须在下一步开始以前一次性完成整个应用程序的工做。若是你的应用程序的某些部分比其余部分慢,这样作的效率不高。这也是几乎全部具备必定规模的应用面临的问题。github
React 18 让你使用 <Suspense>
来将你的应用程序分解成较小的独立单元。这些单元将独立完成这些步骤,而且不会阻碍应用程序的其余部分。所以,你的应用程序的用户将更快地看到内容,并能更快地开始与应用程序交互。你的应用程序中最慢的部分不会拖累那些较快的部分。这些优化是自动的。你不须要写任何特殊的代码来实现这个功能。redis
这也意味着 React.lazy
如今能够和 SSR 一块儿 “正常工做”。这里有一个 demo.数据库
(若是你不使用框架,你将须要改变 HTML 生成的具体方式 wired up。)编程
当用户加载你的应用程序时,你但愿尽快展现一个彻底可交互的页面:
这幅插图用绿色来表达页面的可交互的部分。换句话说,它们全部的 JavaScript 事件处理程序都已经绑定好了,点击按钮能够更新状态等等。
然而,在页面的 JavaScript 代码彻底加载以前,该页面是不能交互的。这包括 React 自己和你的应用程序代码。对于具备必定规模的应用程序,大部分的加载时间将用于下载你的应用程序代码。
若是你不使用 SSR,用户在 JavaScript 加载时看到的惟一东西就是一个空白的页面。
这不是很好,这就是为何咱们建议使用 SSR。SSR 让你在服务器上把你的 React 组件渲染成 HTML 并发送给用户。HTML 的交互性不强(除了简单的内置网络交互,如连接和表单输入)。可是,它能让用户在 JavaScript 仍在加载时看到一些东西。
这里,屏幕中灰色部分表明尚未彻底可交互的部分。你的应用程序的 JavaScript 代码尚未加载完成,因此点击按钮是没有任何响应的。但特别是对于内容繁杂的网站,SSR 很是有用,由于它可让网络链接较差的用户在 JavaScript 加载时开始阅读或查看内容。
当 React 和你的应用代码都在加载时,你要让这个 HTML 是可交互的。你告诉 React:“这是在服务器上生成这个 HTML 的 App
组件。将事件处理程序绑定到该 HTML 上!” React 会在内存中渲染你的组件树,但不是为其生成 DOM 节点,而是将全部逻辑绑定到现有的 HTML 上。
这个渲染组件和绑定事件处理程序的过程被称为 “hydration”。(这就像是用事件处理程序看成 “水” 来浇灌 “干燥” 的 HTML。至少,我是这样向本身解释这个术语的。)
hydration 以后,就是 “React 正常操做”:你的组件能够设置状态,响应点击等等:
你能够看到 SSR 有点像 “魔术”。它不能使你的应用程序更快地彻底可交互。相反,它让你更快地展现你的应用程序的非交互式版本,以便用户在等待 JS 加载时能够查看静态内容。然而,这一招对于网络链接不顺畅的人来讲有很大的不一样,并且提升了总体的感知性能。它还有助于你的搜索引擎排名,既是由于有更容易的索引,也是由于有更快的响应速度。
注意: 不要将 SSR 与服务器组件混淆。服务器组件是一个更具实验性的功能,目前仍在研究中,而且可能不会成为 React 18 最第一版本的一部分。你从这里能够了解服务器组件。服务器组件是对 SSR 的补充,并将成为数据获取的推荐方式之一,但这篇文章并不介绍它们。
上述方法是可行的,但在许多方面,它并非最佳的。
现在 SSR 的一个问题是,它不容许组件 “等待数据”。在目前的 API 中,当你渲染到 HTML 时,你必须已经在服务器上为你的组件准备好全部的数据。这意味着你必须在服务器上收集全部的数据,而后才能开始向客户端发送任何 HTML。这样是很低效的。
例如,假设你想渲染一个带有评论的帖子。尽早显示评论是很重要的,因此你要在服务器的 HTML 输出中包括它们。但你的数据库或 API 层很慢,这是你没法控制的。如今,你必须作出一些艰难的选择。若是你把它们从服务器输出中排除,在 JS 加载完毕以前,用户就不会看到它们。但若是你把它们包含在服务器输出中,你就必须推迟发送其他的 HTML(例如,导航栏、侧边栏,甚至是文章内容),直到评论加载完毕,你才能渲染完整的组件树。这样并很差。
顺便提一下,一些数据获取方案会反复尝试将树渲染成 HTML 并丢弃结果,直到数据被解决。由于 React 没有提供更符合人体工程学的选项。咱们想提供一个不须要如此极端妥协的解决方案。
在你的 JavaScript 代码加载后,你会告诉 React 将 HTML “hydrate” 并使其具备交互性。 React 在渲染你的组件时将 “走” 过服务器生成的 HTML,并将事件处理程序绑定到该 HTML 上。为了使其发挥做用,你的组件在浏览器中生成的树必须与服务器生成的树相匹配。不然 React 就不能 “匹配它们!” 这样作的一个很是不幸的后果是,你必须在客户端加载全部组件的 JavaScript,才能开始对任何组件进行 hydration
例如,假设评论小组件包含不少复杂的交互逻辑,而且须要花费一些时间为其加载 JavaScript。 如今你不得再也不次作出艰难的选择。把服务器上的评论渲染成 HTML,以便尽早显示给用户,这是一个好办法。可是,因为现在的 hydration 只能一次完成,因此在加载评论小组件的代码以前,你不能开始 hydrate 导航栏、侧边栏和文章内容。固然,你可使用代码分割并单独加载,但你必须将注释从服务器 HTML 中排除。不然 React 将不知道如何处理这块 HTML(它的代码在哪里?),并在 hydration 过程当中删除它。
hydration 自己也有一个相似的问题。现在,React 一次性完成树的 hydration。这意味着,一旦它开始 hydrate(本质上是调用你的组件函数),React 就不会中止 hydration 的过程,直到它为整个树完成 hydration。所以,你必须等待全部的组件被 hydrated,才能与任何组件进行交互。
例如,咱们说评论小组件有昂贵的渲染逻辑。它在你的电脑上可能运行得很快,但在低端设备上运行这些逻辑的成本并不低,甚至可能使得屏幕被锁定好几秒钟。固然,在理想状况下,咱们在客户端不会这样的逻辑(这是服务器组件能够帮助解决的问题)。但对于某些逻辑来讲,这是不可避免的。这是由于它决定了所附的事件处理程序应该作什么,并且对于交互性是相当重要的。所以,一旦开始 hydration,用户就不能与导航栏、侧边栏或文章内容互动,直到整棵树完成 hydration。对于导航来讲,这是特别不幸的,由于用户可能想彻底离开这个页面,但因为咱们正忙于 hydration,咱们把他们留在他们再也不关心的当前页面上。
这些问题之间有一个共同点。它们迫使你在早作一些事情(但由于它阻碍了全部其余工做,致使用户体验被损害),或晚作一些事情(但由于你浪费时间,致使用户体验被损害)之间作出选择。
这是由于有一个 “瀑布”(流程):获取数据(服务器)→ 渲染成 HTML(服务器)→ 加载代码(客户端)→ hydration(客户端)。任何一个阶段都不能在前一个阶段结束以前开始。 这就是为何它的效率很低。咱们的解决方案是将工做分开,这样咱们就能够为屏幕的一部分而不是整个应用程序作这些阶段的工做。
这并非一个新奇的想法:好比说:Marko 是实现该模式的一个 JavaScript 网络框架。将这样的模式适应于 React 编程模型具备必定的挑战性。咱们也所以花了一段时间来解决这个难题。咱们在 2018 年为此目的引入了 <Suspense>
组件。当咱们引入它时,咱们只支持它在客户端进行惰性加载代码。但咱们的目标是将它与服务器渲染结合起来,解决这些问题。
让咱们看看如何在 React 18 中使用 <Suspense>
来解决这些问题。
在 React 18 中,有两个主要的 SSR 功能是由 Suspense 解锁的。
renderToString
切换到新的 pipeToNodeWritable
方法,如此处描述。createRoot
,而后开始用 <Suspense>
包装你的应用程序的一部分。为了了解这些功能的做用以及它们如何解决上述问题,让咱们回到咱们的例子。
现在的 SSR 中,渲染 HTML 和 hydration 是 “全有或全无” 的。首先,你要渲染全部的 HTML:
<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>
复制代码
客户端最终会收到它:
而后你加载全部的代码,并对整个应用程序进行 hydration:
可是 React 18 给了你一个新的可能性。你能够用 <Suspense>
来包装页面的一部分。
例如,让咱们包裹评论块并告诉 React,在它准备好以前,React 应该显示 <Spinner />
组件。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
复制代码
经过将 <Comments>
包装成 <Suspense>
,咱们告诉 React,它不须要等待评论就能够开始为页面的其余部分传输 HTML。相反,React 将发送占位符(一个旋转器)而不是评论:
如今在最初的 HTML 中找不到评论了:
<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 代码以前,咱们不能在客户端开始对咱们的应用程序进行 hydration。若是代码的大小很大,这可能须要一段时间。
为了不大型包,你一般会使用 “代码拆分”:你能够指定一段代码不须要同步加载,你的打包工具将把它分割成一个单独的 <script>
标签。
你可使用 React.lazy
进行代码分割,将评论代码从主包中分割出来。
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
复制代码
之前,这与服务器渲染中是不奏效的。(据咱们所知,即便是流行的变通方法也迫使你在选择不使用代码拆分组件的 SSR 或在全部代码加载后对其进行 hydration 之间作出选择,这在某种程度上违背了代码拆分的目的)。
但在 React 18 中,<Suspense>
可让你在评论小组件加载以前就 hydrate 应用程序。
从用户的角度来看,最初他们看到的是以 HTML 形式流进来的非交互式内容。
而后你告诉 React 进行 hydration。虽然评论的代码尚未出现,但也不要紧:
这是一个选择性 hydration 的例子。经过将 Comments
包裹在 <Suspense>
中,你告诉 React,他们不该该阻止页面的其余部分进行流式传输 ——— 并且,事实证实,也不该该阻止 hydration。这意味着第二个问题已经解决了:你再也不须要等待全部的代码加载完成,才能开始 hydration。React 能够在加载部分时同时进行 hydration。
React 会在评论部分的代码加载完毕后开始对其部分进行 hydration:
得益于选择性 hydration,一块沉重的 JS 并不妨碍页面的其余部分具备交互性。
React 会自动处理这一切,因此你不须要担忧事情会以意外的顺序发生。例如,也许 HTML 须要一段时间来加载,即便它正在被流化:
若是 JavaScript 代码的加载时间早于全部的 HTML,React 就没有理由等待了!它将为页面的其余部分进行 hydration:
当评论的 HTML 加载时,由于 JS 尚未出现,因此它将显示为非交互式:
最后,当评论小组件的 JavaScript 代码加载时,页面将变得彻底可交互:
当咱们将评论包裹在 <Suspense>
中时,还有一项改进发生在幕后。如今它们的 hydration 再也不阻碍浏览器作其余工做。
例如,假设用户在评论正在 hydration 时点击了侧边栏:
在 React 18 中,浏览器能够在给 Suspense 里的内容进行 hydration 的过程当中出现的微小空隙中进行事件处理。得益于此,点击被当即处理,在低端设备上长时间的 hydration 过程当中,浏览器不会出现卡顿。例如,这可让用户从他们再也不感兴趣的页面上导航离开。
在咱们的例子中,只有评论被包裹在 Suspense 中,因此对页面的其余部分进行 hydration 是一次性的。然而,咱们能够经过在更多的地方使用 Suspense 来解决这个问题。例如,让咱们把侧边栏也包起来。
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
复制代码
如今二者均可以在包含导航条和帖子的初始 HTML 以后从服务器上流传。但这也会对 hydration 产生影响。比方说,它们的 HTML 已经加载,但它们的代码尚未加载:
而后,包含侧边栏和评论代码的包被加载。React 将尝试对它们进行 hydration,从它在树中较早发现的 Suspense 边界开始(在这个例子中,它是侧边栏):
可是,假设用户开始与评论小组件进行互动,其代码也被加载:
React 会记录点击,并优先给评论进行 hydration,由于它更紧急:
在评论被 hydrated 后,React “重放” 记录的点击事件(经过再次派发),并让你的组件对互动作出反应。 而后,如今 React 没有什么紧急的事情要作,所以 React 会给侧边栏进行 hydration:
这解决了咱们的第三个问题。得益于选择性 hydration,咱们没必要 “为了与任何东西互动而对全部东西进行 hydration”。 React 尽早开始给全部东西进行 hydration。它根据用户的互动状况,优先考虑屏幕上最紧急的部分。若是你考虑到在整个应用程序中采用 Suspense,边界将变得更加细化,那么选择性 hydration 的好处就更加明显:
在这个例子中,用户点击第一条评论时,正好是 hydration 的开始。React 会优先给全部父级 Suspense 边界的内容进行 hydration,但会跳过任何不相关的兄弟节点。由于交互路径上的组件优先被 hydrated,这创造了 hydration 是即时的的错觉。React 会在以后对应用程序的其余部分进行 hydration。
在实践中,你可能会在你的应用程序的根部附近添加 Suspense:
<Layout>
<NavBar />
<Suspense fallback={<BigSpinner />}>
<Suspense fallback={<SidebarGlimmer />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</RightPane>
</Suspense>
</Layout>
复制代码
在这个例子中,最初的 HTML 能够包括 <NavBar>
的内容,但其他的内容会在相关代码加载后当即流入,并分部分进行 hydration,优先考虑用户互动过的部分。
注意:你可能想知道你的应用程序如何能在这种不彻底 hydrated 的状态下运做。设计中有一些微妙的细节,使其发挥做用。例如,不是对每一个单独的组件分别进行 hydration,而是对整个
<Suspense>
边界进行 hydration。由于<Suspense>
已经被用于不会当即出现的内容,因此你的代码对它的孩子不能当即出现的状况有自适应性。React 老是以父级优先的顺序进行 hydration,因此组件老是有它们的 props 组合。 React 在事件发生地的整个父树 hydration 以前,暂不分派事件。最后,若是父类的更新方式致使还没有 hydrated 的 HTML 变得陈旧,React 将隐藏它,并用你指定的fallback
来代替它,直到代码加载完毕。这确保了树在用户面前显得一致。你不须要考虑这个,但这就是该功能发挥做用的缘由。
咱们准备了一个 你能够尝试的演示,看看新的 Suspense SSR 架构如何运做。它被人为地放慢了速度,因此你能够在 server/delays.js
中调整延时。
API_DELAY
让你使评论在服务器上须要更长的时间来获取,展现 HTML 的其余部分如何提早发送。JS_BUNDLE_DELAY
让你延迟 <script>
标签的加载,展现评论小组件的 HTML 如何在 React 和你的应用程序包下载以前 “弹出”。ABORT_DELAY
让你看到服务器 “放弃”,并在服务器上获取时间过长时将渲染工做移交给客户端。React 18 为 SSR 提供了两个主要功能:
<script>
标签一块儿放在正确的地方。这些功能解决了 React 中 SSR 的三个长期存在的问题:
Suspense
组件做为全部这些功能的选择。这些改进自己是在 React 内部自动进行的,咱们指望它们能与大多数现有的 React 代码一块儿使用。这展现了声明性地表达加载状态的力量。从 if (isLoading)
到 <Suspense>
可能看不出很大的变化,但它倒是解锁这些改进的关键。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。