SSR 指的是服务端渲染,直出 html 到客户端,减少首屏白屏时间,这里不对 SSR 过多说明,有兴趣自行查找。

hydration

react 服务端渲染需要经历以下的步骤

  1. 服务器请求数据
  2. 将数据渲染成 html,输出到客户端
  3. 客户端加载 js 代码
  4. 将 js 逻辑与服务器生成的 html 绑定

渲染组件和事件绑定的整个处理过程叫做 hydration。但是整个过程是串行的,必须要等上一步完成才能执行下一步操作。如果项目规模大,某一部分组件生成比较慢,会导致整个网站渲染慢,那体验就很差

React18 会使用 Suspense 来将你的应用程序分解成较小的独立单元。这些单元将独立完成这些步骤,并且不会阻碍应用程序的其他部分。因此,你的应用程序的用户将更快地看到内容,并能更快地开始与应用程序交互。应用程序中最慢的部分不会拖累那些较快的部分。这些优化在 react18 内部是自动完成的

Suspense

既然 SSR 整个过程是依赖强相关的,react18 需要解决两个问题

  • 流式渲染,要使用此功能,必须要从原先的 renderToString 切换到 pipeToNodeWritable
  • 选择性 hydration,在客户端使用 createRoot 方法,用 <Suspense> 包装比较慢的组件(相对不那么重要的组件)
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>

使用  <Suspense>  包装 <Comments /> 组件,react 在服务端渲染的时候,不需要等待 comments 组件完成渲染再返回给客户端,可以先展示加载,上面的代码最终会渲染成

<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>

接下来服务器会继续准备 comment 组件的 html,如果生成结束,就会将额外的 html 发送到同一个流中,并代入一个可执行的 script

<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.lazy

虽然可以提前发送 html,但是首次渲染输出的 js 过大,也需要一些执行时间,通常在客户端渲染无非做两件事,代码拆分,按需加载。

React.lazy 可以在服务端完成这些事了,我们把评论组件的代码从主包中分割出来。

import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>;

这样 react 在除了 comment 组件之外的其他模块,都可以提前 hydration,不需要等待所有代码加载完成再进行 hydration

当多个 Sunspend 一起使用的时候,会出现一个问题,页面加载后优先 hydration 哪部份组件呢?

<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>

现在有 SideBar 和 Comments 组件都被 Suspense 包裹,react 默认会从较早发现的 Suspense 组件开始进行 hydration ,这个例子,是侧边栏

但是用户此时不对侧边进行交互,而优先对评论区域进行交互

React 会记录点击,优先给评论区域进行 hydration ,当完成后,React“重放”记录的点击事件(通过再次派发),并让你的组件对互动做出反应。 接下来再继续给侧边栏进行 hydration

总结

React18 为 SSR 提供了两个主要功能

流式渲染,尽早发送 html,通过 Suspense 实现,Suspense 组件生成的 html 和 script 将会延迟发送给客户端。并在正确位置执行

选择性 hydration 让你在 HTML 和 JavaScript 代码完全下载之前,尽早开始为你的应用程序进行 hydration。它还优先为用户正在互动的部分进行 hydration,创造一种即时 hydration 的错觉

参考资料

New Suspense SSR Architecture in React 18