Open lesenelir opened 3 months ago
React 16 在 SSR 语境下,使用 Suspense 会报错;但是在 React 18,Suspense 可以在 SSR 环境下使用。
一般 SSR 步骤:
The key part is that each step had to finish for the entire app at once before the next step could star
总的来说: fetch data (server) → render to HTML (server) → load code (client) → hydrate (client)
Neither of the stages can start until the previous stage has finished for the app. (无法跳步骤,类似于同步)
由于 “无法跳步骤” 原因,会导致几个问题:
在服务端需要收集完所有数据后才能将 pre-render 的 HTML 发送给客户端
下载加载完所有组件的 js bundle 后才能开始水合。水合个人理解就是执行 js,将 HTML 绑定事件处理函数,在内存中生成 React 的虚拟 DOM,复用 HTML 的 DOM 结构,由 React 进行接管。正因为要保证在内存中生成的虚拟 DOM 和 HTML 的 DOM 结构要匹配(如果不匹配,则水合失败),所以需要加载完所有 js bundle 才开始水合。
所有组件都水合完毕后,才能进行交互。水合一旦开始就无法结束,只有水合完毕后,用户才可以进行交互。
解决:
Breaking the work apart so that we can do each of these stages for a part of the screen instead of entire app. (stages: fetch datat -> render to html ...)
由于 fetch data (server) → render to HTML (server) → load code (client) → hydrate (client) 这个过程不能跳步骤,是一个同步的过程。所以,我们可以将整个 app 应用执行这整个过程拆分为屏幕中的一小部分,让屏幕中的不同小部分来执行上述的过程。即:
React 18 lets you use
主要解决方案: There are two major SSR features in React 18 unlocked by Suspense:
renderToString
to the new renderToPipeableStream
method.hydrateRoot
on the client and then start wrapping parts of your app with <Suspense>
.组件在服务端用 Suspense 组件进行包裹,
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
发送的初始 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 会对 Suspense 包裹的组件,在 SSR 下,会优先渲染 fallback 中的内容,pre-render 的 HTML 会是 fallback 中的 ui。
当 Suspense 组件准备好,服务端在同一个流中流式传输这个组件对应的 HTML,这个 HTML 带有 inline script tag。如下:
<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>
这个过程就是 streaming html ,分块渲染。这个方案解决了第一个问题:不必再等待所有数据都 fetch 完毕后,再拿到完整的 html,而是可以选择进行 延迟 HTML 流式传输 HTML 部分。
Suspense 不仅可以做到流式渲染,还可以做分块水合(选择性水合):
But in React 18,
This is an example of Selective Hydration. By wrapping Comments in
保证了: Hydrating the page before all the HTML has been streamed
如果 HTML 没有流式渲染完,React 也不会去等待 suspense 组件,而是直接水合已经存在的组件。这就是分块水合。(在分开渲染的基础上,又保证了分块水合)
同时,水合完的片段也可以直接进行交互,无需等待所有组件都水合完毕
BTW,suspense loading 是一种 declarative pattern
时间分片、优先级调度
Suspense:
https://github.com/reactwg/react-18/discussions/37