# RSC

前排提示:本文并非纯粹的技术学习文章,融入了很多自己的理解和思考,用大白话说就是会不认同技术理论的某些方面,请读者独立思考,不要轻信作者

RSC,全称 React Server Component,服务端组件,是 Facebook 在 2020-12-22 公布的实验性技术。

它听起来和 SSR 很像,实际也是为了解决 C/S Rendering 的问题,那么它到底解决了什么问题,有没有存在的价值呢?

# 起源

RSC 团队提出了前端开发需要权衡的三个要点:用户体验,可维护性,软件性能。

思考一下这样的场景:Page 中有三个不同的 Block,都需要 uid 来获取不同的数据进行渲染。

基于用户体验考虑,我们希望这三个 Block 的数据同时渲染出来,不要一个 Block 已经渲染完毕了,另外两个依然空空如也;基于软件性能考虑,我们希望能够并行地 fetch 三份不同的数据。此时,我们的解决方案是这样的:

function Page({ uid }) {
	const all = fetchData(uid);
	return (
		<Block1 :data="all.block1" />
		<Block2 :data="all.block2" />
		<Block3 :data="all.block3" />
	)
}

然而,这种方案的可维护性较差,三个理应独立的 Block 耦合在 Page 中,我们如果想添加 Block4 或者撤出 Block1,都需要修改 fetchData 接口。因此,我们常用的做法还是把 Block 单独拆分出来,至于用户体验和性能的问题用各种 trick 去弥补。

针对这种情况,RSC 团队希望能够提供一种比较通用的解决办法,也就是说,我们知道大家自己能解决这些问题,但我们可以提供一种开箱即用的方法,帮你更轻松地解决问题。

想要找到通用的办法,必须先挖出通常的问题。上面这种场景的典型问题就是 waterfall request,整个流程是这样的:

Block1渲染 -> Block1请求数据 -> Block1继续渲染 -> Block2渲染 -> Bloack2请求数据……

如果同一个 Page 上的 Block 特别多,Block99 必须等待前面 98 个 Block 渲染完才能开始渲染,哪怕每个只渲染 0.1s,也要等待将近 10s,这就产生了用户体验问题;如果其中某个 Block 的渲染时间很长(包括网络延时长),就会出现性能瓶颈,这就产生了性能问题。

那么,怎样解决 waterfall 问题呢?第一种方法,拿来,把需要的数据先搬到前端,再让前端自由查询(或许 GraphQL 就是这样的),通过减少数据请求缓解网络延时对渲染的影响,有点像上面展示的联合 fetch,当然可维护性好多了,写起来比较优雅;第二种方法,抛去,把数据获取和渲染的工作抛给服务端,你可能会说,这不就是 SSR 吗?诚然,这种思想与 SSR 基本无二,但现在的前端页面大多不只是静态信息的展示,而是会有大量的 partial update,对此,SSR 的缺点显现:

  • 只有首屏是服务端渲染后返回,后续的 partial update 依然用 ajax+js 完成,如果想要 partial update 也通过 SSR 完成,就会带来频繁的页面刷新,用户体验较差
  • 整个页面完全渲染后才返回,哪怕 SSR 渲染 100 个 Block 只要 3s,但 3s 什么都看不到,相比 10s 内页面一点一点出现,用户很多时候还是愿意选择后者(并无用户数据支撑,仅凭空想)

我们很容易看出,上面这两个缺点其实可以概括为一个粒度问题:SSR 粒度太大,导致许多事只能非黑即白。我们希望获取数据渲染这个过程发生在服务端,但不是页面上的所有内容都需要获取数据渲染,我们可以把 SSR 控制在组件粒度,这就是服务端组件。

# 实现

大致弄懂了服务端组件诞生的背景,让我们来看看 RSC 是如何实现的。

RSC 把组件分为客户端组件和服务端组件两种。客户端组件和我们以前写的 React 组件没有任何区别,A.client.js 会随着 bundle 被传入客户端,被 React 解析后渲染;而服务端组件不会被传到客户端!也就是说,B.server.js 永远不会出现在开发者工具的 Sources 中!

那么,服务端组件是如何被客户端渲染出来的呢?RSC 并不像 SSR 那样返回 HTML 或想象中的 HTML 片段(这就是知乎认为 RSC 不是 SSR 的最大理由),而是返回如下的序列化数据:

其中,J 是渲染一个服务端组件所需要的的全部信息,你可以把它理解为 React 组件在服务端调用 render 后生成的结果,只需在客户端通过 RSC 的库进行一个简单的 paint 就可以渲染上去;M 则是对客户端组件的引用,你可以看到里面有一个客户端组件的路径,在渲染这个包含客户端组件的服务端组件时,并不会将客户端组件也 render 后返回,而是留到客户端再渲染,但是,需要传给这个客户端组件的数据已经完全包含在内了。

这样的渲染方法,主要带来三点优势:

一、RSC 大力宣传的 0 Bundle Size

在渲染组件时,除了 fecth 数据,还常常会用到一些库,比如时间格式处理,或者更大更复杂的库,这些库在以前会随着组件的依赖一同被打包,导致客户端的请求数据量增大,而现在,连服务端组件自己都不打包了,这些库也可以心安理得地待在服务端。

二、有状态的 SSR

SSR 的粒度决定了更新只能通过刷新,那么原页面的状态自然会丢失。想象这样一个场景:你的侧边栏是一个 Expansion Panel,它的数据需要从后端获取,现在你打开了它,但当你获取新的侧边栏数据时,Expansion Panel 丢失了它的状态,它合上了,如果你想让它继续维持打开的状态,不得不借助 localStorage,再写一些生命周期函数里的逻辑;但现在,作为 RSC 的侧边栏更新数据后,它在客户端的打开状态依然保留,客户端渲染侧边栏时会自然地将其渲染成打开的样式。同样的,transition 这种动效也可以在客户端为 RSC 设置。

三、渐进渲染

当服务端渲染好一个 RSC 后,可以立即将其返回给客户端进行渲染,不用像 SSR 一样渲染完整个页面再返回,也不用像最开始的场景里一样顺序地阻塞地渲染组件。

不过,RSC 也有很大的局限性:

一、只能做信息展示,无法与用户交互

由于序列化数据中必须包含服务端组件渲染所需的全部信息,因此,服务端组件的内容必须全部可序列化,连函数都不能带过来,自然不用说与用户交互这些更加复杂的逻辑。但是,这个局限性并不会成为一个缺点,因为这反而是一种好的代码分割规范:与用户交互的任务只能在客户端完成,自然是将其分割为客户端组件;数据渲染在服务端效率更高,自然是将其划分到服务端组件。

二、不支持 SEO

很好理解,RSC 本质上还是存在于一个 React SPA 的框架中,自然对 SEO 无效。但我们可以把 SSR 和 RSC 结合起来,让应用拥有 SEO 的能力,这也是 RSC 不是 SSR 的第二条有力论证。

# 总结

关于 RSC 的其它一些特性或者说语法,比如双端组件,在这里不再赘述,和服务端组件的思想关系不大。

总的来说,我认为 RSC 其实并没有带来效率上的大幅度提升,获取数据、填充数据、传输数据、渲染数据这四步的代价在各种 C/S Rendering 方案中被扔来扔去,但是总量并没有什么大的改变(也无数据支撑,纯空想+1)。

不过这本来就不是 RSC 团队的初衷,他们的目标是优化用户体验、可维护性、软件性能三者的平衡。就这一点来说,我认为 RSC 确实提供了一个更好的开发方式,从一个比较合适的粒度出发拆分了客户端和服务端的职责。当然,实际效果仍需观察。

再想多一点,服务端组件只提供简单的展示,而把复杂的组件内部逻辑、组件依赖的库和数据源都隐藏在服务端,有没有一天会有云组件的诞生?客户端存在的意义,深究到底,本来就只有与用户交互一项,如果能把除了交互之外的所有内容全部抽取到服务端,比 Web 更 Web,这是不是前端进化的最终方向呢?

# 参考

如何看待 React Server Component (opens new window)