lesenelir / read

Logging tech articles and tools I've used.
MIT License
0 stars 0 forks source link

New Suspense SSR Architecture in React 18 #2

Open lesenelir opened 3 months ago

lesenelir commented 3 months ago

https://github.com/reactwg/react-18/discussions/37

lesenelir commented 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. (无法跳步骤,类似于同步)


由于 “无法跳步骤” 原因,会导致几个问题:

  1. 在服务端需要收集完所有数据后才能将 pre-render 的 HTML 发送给客户端

  2. 下载加载完所有组件的 js bundle 后才能开始水合。水合个人理解就是执行 js,将 HTML 绑定事件处理函数,在内存中生成 React 的虚拟 DOM,复用 HTML 的 DOM 结构,由 React 进行接管。正因为要保证在内存中生成的虚拟 DOM 和 HTML 的 DOM 结构要匹配(如果不匹配,则水合失败),所以需要加载完所有 js bundle 才开始水合。

  3. 所有组件都水合完毕后,才能进行交互。水合一旦开始就无法结束,只有水合完毕后,用户才可以进行交互。


解决:

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 to break down your app into smaller independent units which will go through these steps independently from each other and won’t block the rest of the app.

主要解决方案: There are two major SSR features in React 18 unlocked by 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, lets you hydrate the app before the suspense component has loaded.

This is an example of Selective Hydration. By wrapping Comments in , you told React that they shouldn’t block the rest of the page from streaming—and, as it turns out, from hydrating, too! (不会阻止分块渲染,也不会阻止分块水合)

保证了: Hydrating the page before all the HTML has been streamed

如果 HTML 没有流式渲染完,React 也不会去等待 suspense 组件,而是直接水合已经存在的组件。这就是分块水合。(在分开渲染的基础上,又保证了分块水合)

同时,水合完的片段也可以直接进行交互,无需等待所有组件都水合完毕

BTW,suspense loading 是一种 declarative pattern

lesenelir commented 3 months ago

时间分片、优先级调度

lesenelir commented 3 months ago

Suspense: