乐趣区

关于javascript:React-18-中新的-Suspense-SSR-架构

概述

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

次要的新 API 是  pipeToNodeWritable,你能够在 Upgrading to React 18 on the Server 中理解到。咱们打算在细节上做更多的实现,因为这不是最终版本,并且还有一些事件须要解决。

现有的次要的 API 是  <Suspense>.

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

简而言之

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

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

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

关键在于,每一步都必须在下一步开始之前一次性实现整个应用程序的工作。如果你的应用程序的某些局部比其余局部慢,这样做的效率不高。这也是简直所有具备肯定规模的利用面临的问题。

React 18 让你应用  <Suspense>  来将你的应用程序分解成较小的独立单元。这些单元将独立实现这些步骤,并且不会妨碍应用程序的其余局部。因而,你的应用程序的用户将更快地看到内容,并能更快地开始与应用程序交互。你的应用程序中最慢的局部不会连累那些较快的局部。这些优化是主动的。你不须要写任何非凡的代码来实现这个性能。

这也意味着 React.lazy 当初能够和 SSR 一起“失常工作”。这里有一个 demo.

(如果你不应用框架,你将须要扭转 HTML 生成的具体形式 wired up。)

什么是 SSR?

当用户加载你的应用程序时,你心愿尽快展现一个齐全可交互的页面:

这幅插图用绿色来表白页面的可交互的局部。换句话说,它们所有的 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 有哪些问题?

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

在展现任何货色之前,必须先获取所有货色

现在 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 年为此目标引入了 <Suspense> 组件。当咱们引入它时,咱们只反对它在客户端进行惰性加载代码。但咱们的指标是将它与服务器渲染联合起来,解决这些问题。

让咱们看看如何在 React 18 中应用 <Suspense> 来解决这些问题。

React 18:流式 HTML 和选择性 hydration

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

  • 在服务器上流式传输 HTML。要应用这个性能,你须要从 renderToString 切换到新的 pipeToNodeWritable 办法,如此处形容。
  • 在客户端进行选择性的 hydration。要应用这个性能,你须要在客户端 切换到createRoot,而后开始用 <Suspense> 包装你的应用程序的一部分。

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

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

现在的 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 数据获取库提供一种办法来与之集成。

在所有代码加载之前对页面进行 hydration

咱们能够提前发送最后的 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 并不障碍页面的其余局部具备交互性。

在所有的 HTML 都被流化之前,对页面进行 hydration

React 会主动解决这所有,所以你不须要放心事件会以意外的程序产生。例如,兴许 HTML 须要一段时间来加载,即便它正在被流化:

如果 JavaScript 代码的加载工夫早于所有的 HTML,React 就没有理由期待了!它将为页面的其余局部进行 hydration:

当评论的 HTML 加载时,因为 JS 还没有呈现,所以它将显示为非交互式:

最初,当评论小组件的 JavaScript 代码加载时,页面将变得齐全可交互:

在所有组件实现 hydration 之前与页面互动

当咱们将评论包裹在 <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 来代替它,直到代码加载结束。这确保了树在用户背后显得统一。你不须要思考这个,但这就是该性能发挥作用的起因。

Demo

咱们筹备了一个 你能够尝试的演示,看看新的 Suspense SSR 架构如何运作。它被人为地加快了速度,所以你能够在 server/delays.js 中调整延时。

  • API_DELAY  让你使评论在服务器上须要更长的工夫来获取,展现 HTML 的其余局部如何提前发送。
  • JS_BUNDLE_DELAY  让你提早 <script> 标签的加载,展现评论小组件的 HTML 如何在 React 和你的利用程序包下载之前“弹出”。
  • ABORT_DELAY 让你看到服务器“放弃”,并在服务器上获取工夫过长时将渲染工作移交给客户端。

总结

React 18 为 SSR 提供了两个次要性能:

  • 流式 HTML 让你尽早开始发送 HTML,流式 HTML 的额定内容与 <script> 标签一起放在正确的中央。
  • 选择性 hydration 让你在 HTML 和 JavaScript 代码齐全下载之前,尽早开始为你的应用程序进行 hydration。它还优先为用户正在互动的局部进行 hydration,发明一种即时 hydration 的错觉。

这些性能解决了 React 中 SSR 的三个长期存在的问题:

  • 你不再须要期待所有的数据在服务器上加载后再发送 HTML。相同,一旦你有足够的数据来显示应用程序的外壳,你就开始发送 HTML,其余的 HTML 在筹备好后再进行流式传输。
  • 你不再须要期待所有的 JavaScript 加载来开始 hydration。相同,你能够应用代码拆分和服务器渲染。服务器 HTML 将被保留,React 将在相干代码加载时对其进行 hydration。
  • 你不再须要期待所有的组件被 hydrated 后才开始与页面互动了。相同,你能够依附选择性 hydration,来优先思考用户正在与之互动的组件,并尽早对它们进行 hydration。

Suspense 组件作为所有这些性能的抉择。这些改良自身是在 React 外部主动进行的,咱们冀望它们能与大多数现有的 React 代码一起应用。这展现了申明性地表白加载状态的力量。从 if (isLoading)<Suspense> 可能看不出很大的变动,但它却是解锁这些改良的要害。


  • 原文地址:New Suspense SSR Architecture in React 18
  • 原文作者:[]()
  • 译文出自:掘金翻译打算
  • 本文永恒链接:https://github.com/xitu/gold-miner/blob/master/article/2021/new-suspense-ssr-architecture-in-react-18.md
  • 译者:NieZhuZhu(弹铁蛋同学)
  • 校对者:Kimberly、Zavier
退出移动版