写在前面
2020 年的圣诞节前,React 团队放出了 Server Components 的相关消息,而此前,我恰好在研究 SSR(Server-Side Rendering,服务端渲染),并对Next.js 的混合渲染赞叹不已
其实 Server Components 也确实与 Server-Side Rendering 有着千丝万缕的联系,毕竟两者都带个 Server 嘛(认真脸,这一点很重要)。得益于此前对 SSR 的一系列研究,所以,从某种程度上来讲,我更能够深刻理解 Server Components 背后的思考和设计考量
一.Server Components 是什么?
Server Components are a new type of component that we’re introducing to React and this tells React that we only want this component to ever render on the server.
(摘自Data Fetching with React Server Components演讲视频)
官方定义,Server Components 本质上是一种新的 React 组件(像 Portal、Fragment 等组件类型一样),其特别之处在于这种组件只在服务端运行
可是,React 组件在客户端跑得好好的,怎么突然说要拿到服务端去运行呢?Hooks 还没学明白,一大波服务端组件的更新又要来了,React 团队这帮人到底在搞什么?
二.为了解决什么问题?
按RFC上写的,React 引入 Server Components 概念能够解决一大把问题:
Zero-Bundle-Size Components:打包产物大小的问题
Full Access to the Backend:数据访问、传递需要先考虑组件树结构的问题
Automatic Code Splitting:代码拆分的性能优化需要手动改造的问题
No Client-Server Waterfalls:请求顺序与组件树结构强关联的问题
Avoiding the Abstraction Tax:多层抽象带来的性能问题
诸如此类,但这些看起来更像是从解决方案倒推出来的收益,而我更关注的是其初衷,最初最想解决的问题是什么?
First, we wanted to make it easier for developers to fall into the “pit of success” and achieve good performance by default. Second, we wanted to make it easier to fetch data in React apps.
(摘自Motivation)
初衷是想解决两大类问题:
第一类:性能优化相对复杂,能否默认就实现高性能?
第二类:在 React 应用中取数据其实是有不少顾虑的,有没有更简单、更优雅的办法?
性能优化比如按需引用类库、按路由拆分代码、数据请求提前、减少过度抽象等等,这些优化措施都需要手动改造,给应用开发造成了一定的复杂性。另一方面,为了高性能,通常把数据请求提升到顶层,导致数据展示组件无法清晰地对应到数据源上
例如我们常见的:
// 数据展示组件的数据源依赖不清晰
function ArtistPage({ artistID }) {
const artistData = fetchAllTheStuffJustInCase();
return (
<ArtistDetails
details={artistData.details}
artistId={artistId}>
<TopTracks
topTracks={artistData.topTracks}
artistId={artistId} />
<Discography
discography={artistData.discography}
artistId={artistId} />
</ArtistDetails>
);
}
(出于代码维护性考虑)更想要的是:
// 类似这样的清晰依赖,每个组件明确知道其数据从哪来
function ArtistDetails({ artistId, children }) {
const artistData = fetchDetails(artistId);
// ...
}
function TopTracks({ artistId }) {
const topTracks = fetchTopTracks(artistId);
// ...
}
function Discography({ artistId }) {
const discography = fetchDiscography(artistId);
// ...
}
对于不断追寻极致用户体验和开发体验的 React 团队而言,更简单、更优雅的数据获取方式是其一直以来的探索方向(如Suspense for Data Fetching (Experimental))。因此,实际情况可能是 React 团队在解决数据获取问题时,提出了 Server Components 的思路,一拍脑门发现这种大胆的想法还能顺道解决许多性能问题,于是就有了客户端到服务端的跃迁式新特性预告……
三.Server Components 是怎么解决的?
要解决组件的数据关联问题,就要让组件有各自清晰的数据源,而瀑布式请求又会带来性能问题……好用户体验、低维护成本、高性能似乎难以三者兼具,但也并非不可能兼具,至少 React 团队已经探索除了两种解法:
Relay + GraphQL:Relay 框架配合 GraphQL 特性,对散落各处的数据请求进行合并,从而解决性能问题
Move our components to the server:把组件搬到服务器上去运行,降低数据请求的成本,从而(在很大程度上)解决性能问题
GraphQL能够根据请求指定的数据模型(schema)轻松拼装数据片段,配合 Relay 框架将多次请求合并成一次,既保留了组件源码的维护性(清晰的数据源依赖),又避免了由此产生的性能问题,但可惜的是强依赖 GraphQL,不算是个真正意义上的通用解决方案
而Server Components 的路子相对狂野些,为了降低多次客户端请求的时间开销,干脆把组件放到服务器上运行,而(同单元)服务器间的数据通信是相当快的,这时候多次数据请求的性能开销便不足为惧了,并且最终会在(框架层)引入数据缓存机制后得到彻底解决
等等,把组件搬到服务器上运行,不就是 SSR 么?这莫非是朝花夕拾?
四.Server Components 与 SSR 的关系?
联系
一般来讲,传统的 SSR 免不了两个过程,服务端渲染 + 客户端 hydrate 二次渲染:
服务端渲染:在服务端渲染出首屏内容(HTML 字符串)
客户端 hydrate 二次渲染:在客户端加载完首屏内容(此时页面不可交互)、以及前端应用的完整代码之后,进行一次类似于 render 的 hydrate 二次渲染过程,把交互事件绑上去(此时页面可交互)
Server Components 的渲染过程与之类似:
服务端渲染:在服务端渲染出首屏内容(一种中间形式,同样是用来描述 UI 的)
客户端渲染:接到服务端输出的中间形式,从头 render,开始流式渲染
所以,Server Components 与 SSR 的联系至少有以下几点:
- 都在服务端执行组件渲染逻辑(所以都不支持交互)
- 都允许同一组件跨客户端、服务端运行(Shared Components)
- 都从客户端延伸到服务端寻求更多的性能突破
对于二者之间的关系,React 官方有一个词表述得很到位,Server Components 与 SSR 是互补的(complementary),双剑合璧,SSR 能把首屏渲染成 HTML 加速内容展示,Server Components 能够帮助减少 hydrate 二次渲染所需加载执行的代码量(Server Components 只在服务端渲染,相关代码不需要在客户端加载执行),进而加快页面的可交互时间:
You can combine Server Components and SSR, where Server Components render first, with Client Components rendering into HTML for fast non-interactive display while they are hydrated. When combined in this way you still get fast startup, but you also dramatically reduce the amount of JS that needs to be downloaded on the client.
区别
关键区别有 3 点:
Server Components 相关代码根本不会给到客户端,而传统 SSR 所有组件代码全都要打进客户端 bundle 里
Server Components 允许在组件树的任意位置直接访问后端,传统 SSR 只允许在顶层(页面级)获取数据
Server Components 在更新时能保留客户端交互状态(包括输入的搜索词、滚动位置、焦点、选中内容等等),因为 Server Components 渲染结果是一种比 HTML 信息更丰富的中间格式(毕竟 HTML 只能表达 HTML,自定义格式则不存在这个限制,比如能带上 props)
Server Components 只在服务端执行,客户端并不加载这些代码,服务端给到客户端的始终只是 Server Components 的渲染结果,包括二次更新,以中间形式给到客户端后,客户端只把来自服务的渲染结果 merge 到当前已经渲染好的客户端组件上,所以能保留交互状态:
Specifically, React merges new props passed from the server into existing Client Components, maintaining the state (and DOM) of these components to preserve focus, state, and any ongoing animations.
P.S.关于二者区别的详细信息,见danabramov on Zero-Bundle-Size React Server Components
五.Server Components 的优势
1.有利于减小 bundle size
因为 Server Components 只在服务端运行,组件本身及其依赖库都不打进客户端 bundle 中,所以能在很大程度上缩减包体积(Facebook 的试点案例减小了 30%左右)
另一方面,中间的多层抽象封装都在服务端被消化掉了,减轻了客户端的负担
2.能在组件树的任意位置访问后端资源
能在组件树的任意位置访问后端资源,这在传统 SSR 中也是做不到的,因为传统 SSR 缺少客户端框架配合,只能要求数据一次性拿回来,然后进行一次同步的组件渲染,最后将结果给到客户端
实际上,初衷是为了让组件与其数据源的关系更清晰,代码可维护性更好:
// 类似这样的清晰依赖,每个组件明确知道其数据从哪来
function ArtistDetails({ artistId, children }) {
const artistData = fetchDetails(artistId);
// ...
}
function TopTracks({ artistId }) {
const topTracks = fetchTopTracks(artistId);
// ...
}
function Discography({ artistId }) {
const discography = fetchDiscography(artistId);
// ...
}
修改组件时一同修改对应数据,下线组件时连同数据请求一起下掉
3.能够按需下载代码
因为服务端有数据,确切知道需要下发哪些组件:
Server Components let you only download the code that you actually need, they enable automatic code splitting for client code with support of a bundler plugin, you can use as much Server Components or as little as you like.
同时让自动的代码拆分成为了可能,所有 Client Components import 自动按需加载,不再需要逐一采用 dynamic imports,开发者不用显式关注,Server Components 默认支持
4.与 SSR 互补
单从 SSR 的角度来看,Server Components 是组件化框架从组件系统层面解决了 SSR 应用框架解决不了的问题,比如:
加快 hydrate 二次渲染(减少客户端所需加载执行的代码量,曲线救国)
流式渲染支持(不同于 SSR 流式输出,流式渲染一定是需要组件化框架本身配合服务端的)
允许局部刷新,保留交互状态(传统 SSR 只能用作首屏)
这些性能点单靠 SSR 框架是没有办法做到极致的,而 Server Components 大大加速了这一进程
另一方面,开篇提到 Next.js 在混合渲染方面进行了深入地探索,允许 SSG、SSR、CSR 以多种方式混用,抓住一切机会进行预渲染,其目的是提升首屏性能(包括 SPA 路由跳转等交互场景下的首屏性能)。因此,从某种意义上来讲,Server Components 与这些预渲染探索殊途同归,也因此并不冲突能够配合使用
六.发展现状
虽然早在去年就在 Facebook 内部进行了试点,但只是初步验证,离生产化还有一定距离
并且因为把组件搬到服务端去运行,涉及构建、服务端渲染、路由控制等诸多环节,超出了组件化框架的范畴,所以 React 团队计划与 Next.js 团队合作共建,先尝试与 Next.js 进行集成(当然,Server Components 并不仅限于某个特定 SSR 框架使用,只是先集成一个试试)
目前可通过官方 Demo进行试玩,需要注意的是,由于 Demo 依赖 postgre 数据库,建议通过 docker 启动:
docker-compose up -d
docker-compose exec notes-app npm run seed
通过试玩能了解到一些细节,比如 Server Components 渲染而来的中间格式大概长这样:
其中,Client Components 以 bundle 索引的形式返回,原生组件(div、span 等)以 JSON 格式返回,例如:
[
"$",
"header",
null,
{
"className": "sidebar-note-header",
"children": [
["$", "strong", null, { "children": "todo" }],
["$", "small", null, { "children": "5/4/21" }]
]
}
]
P.S.关于 React Server Components 的更多技术细节,见RFC: React Server Components