xiaoxiaojx / blog

Project for records problems solved in my work and study.
https://xiaoxiaojx.github.io/
MIT License
252 stars 6 forks source link

从业务的角度来看 React18 Suspense SSR 架构 #50

Open xiaoxiaojx opened 1 year ago

xiaoxiaojx commented 1 year ago

image

目录

1. 实际业务的困境

现有的服务端渲染(Server-side rendering,简称 SSR)的原理是当 HTML 请求到达 Node 端时先等待后端接口数据请求完成(30~300ms),然后再进行渲染(2~5ms),最后再响应渲染完成的页面给浏览器。

大致流程是: fetch data (server) → render to HTML (server) → load code (client) → hydrate (client)

如本文用作示例的商品管理页面,需要并发8个后端接口请求,最慢的接口 /api/xxx/goodsList 延时为 246.6 ms,导致Step1阶段用户看到的页面白屏时间至少是 246.6ms + 5ms

image

💡 Step2 截图为灰色仅为了区别于 Step3 可交互状态,实际用户看到的效果与 Step3 无差异

为了解决后端接口延时不可控造成的 Step1 阶段白屏时间过长的问题,于是我们开发了渐进式渲染功能,优化后的渲染链路变成了如下

image

2. Suspense SSR 架构

React18 新的 Suspense SSR 架构允许你在服务端使用 Suspense 组件,比如你的 Comments 组件是需要后端接口的数据,那么可以做到后端接口数据仅阻塞 Comments 组件,不会阻塞整个 App 组件的渲染与提前返回

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

新 Suspense SSR 架构下的渲染链路变成了如下

image

2.1. 可能存在的问题

你可能想到部分可交互状态时,如果客户端其他组件响应了事件导致 Comment 组件的 props 变化,而服务端是根据 initProps 对 Comment 进行的渲染,那么 React 会如何取舍

function Content() {
  const [count, setCount] = useState(0);

  return (
    <Layout>
      <NavBar />
      <Sidebar />
      <RightPane>
        <Post />
        <h2
            onClick={() => {
              setCount(count + 1)
              console.log("setCount 点击事件测试, count: ", count);
            }}
          >
            setCount 点击事件测试
        </h2>
        <Suspense fallback={<Spinner />}>
          <Comments count={count}/>
        </Suspense>
      </RightPane>
    </Layout>
  );
}

function Comments({ count }) {
  const comments = useData();

  return (
    <>
    <span>count: {count}</span>
      {comments.map((comment, i) => (
        <p className="comment" key={i}>
          {comment}
        </p>
      ))}
    </>
  );
}

从测试结果来看 Props 发生变化后 React 会以客户端最新渲染的结果为准, 与此同时抛出Uncaught Error: This Suspense boundary received an update before it finished hydrating.错误

image

3. 应用到业务中的效果

因为 Suspense 支持对于单个组件进行的延迟渲染,首先我们需要对页面组件进行拆分,同时使用 Suspense 进行包裹

image

如果升级到了新 Suspense SSR 架构下的渲染链路变成了如下

image

4. 小结

Suspense SSR 架构解决了服务端渲染各个流程串行等待问题,强调一切按需(懒加载,懒编译,现在是懒渲染?)进行
渐进式渲染首屏比 Suspense SSR 更加完整
Suspense SSR 类似于懒渲染,设计理念更加符合现代化 Web 开发

5. 最后的话

如果发现升级后页面没有进行分块渲染, 或许你要继续阅读 👉 服务端流式渲染 iOS 中踩坑记

Edge00 commented 1 year ago

因为推荐类接口耗时都比较长,被迫从 SSR 出内容改成了 SSR 出骨架屏,hydration 后再从 client 发起请求。 这个新的架构下就可以更早发起接口请求了👍🏻

xiaoxiaojx commented 1 year ago

推荐类 Case 较低成本的是先实现我文章所提到的渐进式渲染,接口在内网聚合还是比客户端快不少

Suspense SSR 在 Next.js 中也只是半成品,如下图,Next.js 实现是等所有 Suspense 组件就绪后最后统一发送给客户端,没有做到按需。我这两天在内部SSR框架实现了一版,发现 React18 支持的东西太少,大都需要框架自行去实现解决 😓 c24fc0759707d802ecf1e7f5b6154a7da2e7428c