抛砖引玉React Server Components
2023-8-15
| 2023-8-16
字数 7248阅读时长 19 分钟
beizhu
type
Post
status
Published
date
Aug 15, 2023
slug
rsc
summary
本文从React和Next.js 两个角度浅入浅出、浅尝辄止的看一下RSC,主要的篇章在第一部分React角度,Next文档已经比较完善了,故Next方面的RSC介绍的稍微少点
tags
Next.js
category
技术
icon
password

一、React Server Components

1.1 What React Server Components?

一听到Server Components,一听,这不就是服务器端渲染(SSR)嘛。它们的名称中都有“server”,并且它们都在服务端进行工作。但把它们视为是两个独立且不相关的特性会更容易理解。使用 RSC 不需要使用 SSR,反之亦然!SSR 模拟了一个环境,用于将 React 树渲染为纯 HTML;它不区分服务端和客户端组件,并且以相同的方式渲染它们

组件类型RSC、RCC

首先React Server Components 是一种新组件,过往我们熟悉的组件被称为客户端组件,它还是一个半实验性质的功能,在next13.4版本(23年5月)后next团队发布成了稳定版本, 不过不出意的话这个功能会是 React 未来发展的方向,也许(Maybe)会重新定义前端与后端的分工,也会改变我们熟悉的 React 开发方式。
 
对于一个React组件,可能包含两种类型的状态:
  • 前端交互用的状态,比如loading的ture/false状态
  • 后端请求返回的数据,比如下面代码中的data状态用于保存后端数据:
拆解一下:
  1. 前端请求并加载React业务逻辑代码。
  1. 应用执行渲染流程。
  1. App组件mount,执行useEffect,请求后端数据。
  1. 后端数据返回,App组件的子组件消费数据。
如果我们根据「状态类型」将组件分类,比如:
  • 「只包含交互相关状态」的组件,叫客户端组件(React Client Component,简写RCC)。
  • 「只从数据源获取数据」的组件,叫服务端组件(React Server Component,简写RSC)。
按照这种逻辑划分,将上述代码改写为
  • App组件在后端运行,可以直接从数据源(这里是数据库)获取数据
  • Item组件在前端运行,消费数据
概括就是 —— 根据状态类型,划分组件类型,RCC在前端运行,RSC在后端运行。
 
在这个模式下,我们可以将组件分为以下三种:
  • Server Components:在Server Side 渲染的元件,具有访问DB、file system 的能力,但没办法做事件绑定,也就是缺少了「交互性」。
  • Client Components:在Client Side 渲染的元件,拥有交互性。
  • Share Components:可以在Server Side 也可以在Client Side 渲染,具体要看是什么组件引入它,如果被Server Components 引入就由server 渲染,反之则由client 渲染。
React Component Tree 看成它是由Server Side 与Client Side 混合渲染的一个树状结构,React 在Server Side 将Server Components 渲染好后传给client 端,如果Server Side在渲染的过程遇到Client Components,它就会用一个Placeholder 来标注它(ps: 它不会实际执行或渲染它),未来让Client Side 知道这是需要它来渲染的元件。一般来说Client 端在接收到JS bundle 后会进行hydration,不过Server Components 只会在Server Side 做渲染,不会在client 端进行水合。
 
notion image
notion image
💡
PS:RCC中是不允许import RSC的。如下写法是不支持的:
那为什么上面RSC 下面挂载了 RCC呢?
 
那么在设计客户端组件时,就可以使用 React props 来标记服务器组件的“slot”。插槽的方式
Example:
OuterServerCpnRSC,则他运行的环境是后端。他引入的ServerCpn组件运行环境也是后端。
ClientCpn组件虽然运行环境在前端,但是等他运行时,他拿到的children props是后端已经执行完逻辑(已经获得数据)的ServerCpn组件。

1.2 RSC原理

总体概括

根服务器组件呈现为基本 html 标签和客户端组件“placeholders”的树。然后序列化这棵树,将其发送到浏览器,浏览器可以完成反序列化的工作,用真实的客户端组件填充客户端占位符,并渲染最终结果。
服务端和客户端并不是割裂的,两者是并行发生在两个不同的世界。

序列化 JSON

按照上面的示例 - 假设我们想要渲染<OuterServerComponent/>. 我们能JSON.stringify(<OuterServerComponent />)得到一个序列化的元素树吗?
 
好像 大致 也许是可以,原理上的确是这样的😂
当您有一个组件(不是基本 html 标签)时,该type字段引用组件函数,并且函数不可 JSON 序列化!
为了正确地将所有内容进行 JSON 字符串化,React 传递了一个特殊的函数来JSON.stringify()正确处理这些组件函数引用;resolveModelToJSON()您可以在 中ReactFlightServer.js找到它。
notion image
 
这个函数简单来说, React 元素被序列化时,
  • 如果它是一个基本的 html 标签(type"``div``"字段是一个像这样的字符串),那么它已经是可序列化的!没什么特别可做的。
  • 如果是服务器组件,则用Props调用服务器组件函数(存储在 type 字段中),并对结果进行序列化。这实际上是在"渲染"服务器组件;目标是将所有服务器组件转换为基本的 HTML 标签。
  • 如果它是用于客户端组件,那么......它实际上也已经是可序列化的!type字段实际上已经指向模块引用对象,而不是组件函数。
    • 例如,一个ClientComponent元素可能看起来像这样:
这个由打包工具来完成的!React团队在react-server-dom-webpack中为webpack提供了官方的RSC支持,可以作为webpack loadernode-register来使用。当服务器组件从*.client.jsx文件中导入某个内容时,实际上它只获取到一个模块引用对象,其中包含了该内容的文件名和导出名称。在服务器上构建的React树中从未包含客户端组件函数。
 
回到 最上面那个<OuterServerComponent /> Example
在此过程结束时,我们希望最终得到一个在服务器上看起来更像这样的 React 树,并将其发送到浏览器以“完成”:
notion image
所有的Props 都必须被序列化
 
因为我们将整个 React 树序列化为 JSON,所以传递给客户端组件或基本 html 标签的所有 props 也必须是可序列化的。这意味着从服务器组件中,不能将事件处理程序作为 props 传递!
 
在 RSC 过程中,当我们遇到客户端组件时,永远不会调用客户端组件函数,或者调用到客户端子组件。如果有一个客户端组件实例化另一个客户端组件
在这个 RSC JSON 树中,根本不会出现 ClientComponent2 ,只会看到有模块引用和用于 ClientComponent1 的 props 的元素。

浏览器重新建构React Tree

浏览器从服务器接收 JSON 输出,现在必须开始重建要在浏览器中呈现的 React 树。type每当我们遇到模块引用的元素时,我们都会希望将其替换为对真实客户端组件函数的引用。
这需要我们的打包工具的帮助;在服务器端,正是我们的打包工具将客户端组件函数替换为模块引用,而现在我们的打包工具知道如何在浏览器中将这些模块引用替换为真正的客户端组件函数。
重建的 React 树看起来像这样——只交换了原生标签和客户端组件:
notion image
然后我们像往常一样渲染并提交这棵树到 DOM

RSC格式

RSC 按行分隔的数据结构(方便按行流式传输),每行的格式为:
  • 标记代表这行的数据类型,比如J代表组件树M代表一个RCC的引用S代表Suspense
  • id代表这行数据对应的id
  • JSON数据保存了这行具体的数据
RSC的序列化与反序列化其实就是JSON的序列化与反序列化。反序列化后的数据再根据标记不同做不同处理。
 
RSC协议id映射的完整过程:
  1. 业务开发时通过.server | client后缀区分组件类型
  1. 后端代码编译时,所有RCC(即.client后缀文件)会编译出独立文件,这一步是react-server-dom-webpack插件做的
  1. React后端返回给前端的RSC数据中包含了组件树(J标记)等按行表示的数据
  1. React前端根据J标记对应数据渲染组件树,遇到引用RCC(形如M[id])时,根据id发起请求
  1. 请求返回该RCC对应组件代码,请求过程的pending状态由<Suspense/>展示
 
💡
PS: next.js 相关react-server-dom-webpack在源代码中
 

1.3 简易实现一个RSC

1.4 RSC对React为什么重要

随着React的发展,React已经变得很依赖于Next。但我个人有一点不太能想通,大家友好讨论下。
React毕竟是Meta官方孵化出来的开源项目,React现在看趋势在发展成为底层框架,从React18暴露的一些hook就可以出来,一些hook完全不是给个人开发者使用的,明显是给应用级别框架提供一些优化的钩子。虽然很多React核心成员都去了Vercel,React核心团队还是很多Meta的开发人员,Meta就放任React完全偏向Vercel吗?
虽然Vercel conf开会,提到Remix(Shopify)、Gatsby(Netlify), 但看趋势是React肯定后续会偏向到服务端。
 
为什么RSC对React这么重要?要回答这个问题,得从开源项目的发展聊起。
开源项目要想获得成功,一定需要满足目标用户(开发者)的需求。
早期,React作为前端框架,满足了UI开发的需求。在此期间,React团队的迭代方向主要是:
  • 摸索更清晰的开发范式(发布了Error Boundray、Suspense、Hooks)
  • 修补代码(发布新的Context实现)
  • 优化开发体验(发布CRA)
  • 底层优化(重构Fiber架构,优化Scheduler)
可以发现,这些迭代内容中大部分(除了底层优化)都是直接面向普通开发者的,所以React文档(文档也是面向开发者的)中都有体现,开发者通过文档能直观的感受到React不断迭代。
随着前端领域的发展,逐渐涌现出各种业务开发的最佳实践,比如:
  • 状态管理的最佳实践
  • 路由的最佳实践
  • SSR的最佳实践
一些框架开始整合这些最佳实践(比如Next.js、Remix,或者国内的Umijs...)
到了这一时期,开发者更多是通过使用这些框架间接使用React。
感受到这一变化后,React团队的发展方向逐渐变化 —— 从「面向开发者」的前端框架变为「面向上层框架」的元框架。
发展方向变化最明显的表现是 —— 新版文档中新出的特性普通开发者很少会用到,比如:
  • useTransition
  • useId
  • useMutableSource
这些特性都是作为元框架,给上层框架(或库)使用的。
上述特性虽然普通开发者很少用到,但至少文档中提及了。但随着React不断向元框架方向发展,即使出了新特性,文档中已经不再提及了。比如:
  • useOptimistic
  • useFormStatus
上述两个Hook想必大部分同学都没听过。他们是React源码中切实存在的Hook。但由于是元框架理念下的产物,所以React文档并未提及。相反,Next.js文档中可以看到使用介绍。
 

1.5 RSC优点

React Server Component的目的是让开发者能够构建跨越服务器和客户端的应用,结合的丰富交互性和传统服务器渲染的优化性能。React Server Component可以解决一些现有技术无法解决或者解决不好的问题,例如:
  • 零包大小:React Server Component的代码只在服务端运行,永远不会被下载到客户端,因此不会影响客户端的包大小和启动时间。而客户端只接收RSC渲染完的结果。基本客户端运行时的大小是可缓存且可预测的,并且不会随着应用程序的增长而增加
  • 完全访问后端:React Server Component可以直接访问后端的数据源,例如数据库、文件系统或者微服务等,而不需要通过中间层来封装或者转换。
  • 自动代码分割:React Server Component可以动态地选择要渲染哪些客户端组件,从而让客户端只下载必要的代码。
  • 无客户端-服务器瀑布流:React Server Component可以在服务器上加载数据并作为props传递给客户端组件,从而避免了客户端-服务器瀑布流问题。 避免抽象税:React Server Component可以使用原生的JavaScript语法和特性,例如async/等,而不需要使用特定的库或者框架来实现数据获取或者渲染逻辑。
 

1.6 RSC弊端

  • 使用React Server Components 还会有一点心智负担的,RSC 这块对于没有服务端开发经验的前端存在上手门槛(开发模式、新的 API 以及为什么),什么时间用 “use client”,什么时间 ”server-only““client-only”,哪部分做客户端渲染,哪部分做服务端渲染。组件职责和颗粒度划分不清楚,客户端组件和服务器端组件混用,到时候写起来代码估计会比较乱,不说乱吧,估计到时候报错都能整懵逼,当然这也是我自己目前的猜测😜
  • 状态管理这块和上述第一个问题一样,感觉也可能是一个问题,会较为混乱。
  • RSC 现在只是证明能做读数据库之类的事儿,但不代表它这样做就是最好的,至少在我看来,各种副作用逻辑,不是说你一个 await 或者靠新加一个use这种 hook 就解决了的
  • RSC不支持持续不断的更新,比如通过WebSockets。在这些场景下,一个客户端的请求或轮询是合理的解决方法。
  • 对之前 API 层面的约定侵入性太强了,诸如 RSC 不能用 useEffect, useContext 等等一系列的约定,以及use hook 的语义之类的,按我的理解,约定做的太多,等于没有约定,这个东西确实对开发者造成了一定程度上的心智负担
  • React Server Components 破坏了几乎所有现有的 React 第三方库第三方包,是完全不支持RSC的,使用需要封装在强制客户端渲染的组件中use client
  • React DevTools 不显示 React Server 组件的详细信息

1.7 RSC总结

RSC 并不是要取代客户端组件。健康的应用程序利用 RSC 进行动态数据获取,并利用客户端组件来实现丰富的交互性。挑战在于确定何时使用每个组件。
作为开发人员,请考虑利用 RSC 进行服务器端渲染和数据获取,同时依靠客户端组件来实现本地交互功能和用户体验。通过取得适当的平衡,您可以创建高性能、高效且引人入胜的应用程序。
最重要的是,您继续在非标准环境中测试您的应用程序:模拟较慢的计算机、较慢的手机和较慢的 WiFi,您可能会惊讶地发现您的应用程序在正确的组件组合下运行得更好。
RSC 并不是解决用户过多客户端 JavaScript 负担问题的完整解决方案,但它们确实使我们能够选择何时将计算量转移到用户设备上。

二、Next.JS App Router

React core team 把server components 交给vercel 的next.js 团队去做了。
从next 13.0 rsc beta版 → next 13.4 stable稳定版
notion image

从SSR更新颗粒度来说:

  1. Next.js Page Router - 页面级方法,例如getServerSideProps()getStaticProps()。它们无法为各个组件提供足够细粒度的控制,并且往往会过度获取数据。(当用户导航到该页面时,无论他们实际与哪些组件交互,都会获取所有数据。)
  1. Qwik - 组件级方法。它把将所有必需的信息序列化为 HTML 的一部分(事件处理函数内容、应用状态、框架状态)。事件冒泡来拦截所有事件的全局事件处理程序
    1. 核心思路是通过更加细粒的代码控制配合惰性加载事件处理程序以及事件委托来缩短首屏 TTI。
  1. RSC颗粒度介于二者之间
    1. notion image
划分颗粒度的本质,也就是性能的权衡 —— 如果将尽可能多的逻辑放到后端,那么前端页面需要加载的JS代码(逻辑对应的代码)就越少,那么前端花在加载JS资源上的时间就越少。
但是另一方面,如果划分的粒度太细(比如中或细粒度),可能意味着:
  • 更大的后端运行时压力(毕竟很多原本前端执行的逻辑放到了后端)
  • 降低部分前端交互的响应速度(有些前端交互还得先去后端请求回交互对应代码再执行)
notion image
notion image
 
notion image
 

APP Router

在路由段的特殊文件中定义的 React 组件在特定的层次结构中呈现:
  • layout.js
  • template.js
  • error.js(react错误边界)
  • loading.js (React suspense边界)
  • not-found.js(react错误边界)
  • page.js 或嵌套 layout.js
    • notion image
嵌套路由
notion image

部分渲染

在同级路由(例如/dashboard/settings/dashboard/analytics下方)之间导航时,Next.js 将仅获取和呈现更改的路由中的布局和页面。它不会重新获取或重新呈现子树中段上方的任何内容。
notion image
如果没有部分呈现,每次导航都会导致整个页面在服务器上重新呈现。仅渲染正在更新的片段可减少传输的数据量和执行时间,从而提高性能。
 
 
notion image
服务端来主导了路由以及整个项目

组件颗粒度划分

 
notion image
 
notion image

Why React Server Components?

允许在服务器端更高效地渲染动态内容。这可以极大地改善网络应用的性能和可扩展性,特别是那些在服务器端渲染方面依赖重大的应用。通过将更多的渲染工作转移到服务器端,客户端的 JavaScript 可以专注于交互性和用户体验,从而使应用程序更加流畅和响应。
使用服务器组件,初始页面加载速度更快,客户端 JavaScript 包大小减小。基本客户端运行时在大小上是可缓存可预测的,并且不会随着应用程序的增长而增加。
notion image
 
 
 

三、番外篇

PS(not me):2015 年,我们开始使用 React 来完成所有前端工作。简单的架构、对组件的关注以及无论代码库大小如何都能保持稳定的生产力使其成为一个简单的选择。React 非常受欢迎,社区发展得非常快。最近,React 和 Next.js 团队一直在推广Server Components,这是一种构建 Web 应用程序的新方法,但不适合大多数现有的 React 应用程序。
 
React → Next.js → RSC → Vercel云服务
  • Next.js 官方文档主要建议从 13.4 版本开始使用 React Server Components 。React Server Components 是根据官方文档构建 React 应用程序的默认方式。
notion image
 
[3]不禁觉得Next.js采取的新方向并不是为了帮助开发者,而是为了帮助Vercel销售React。你不能真正为SPA(单页应用)销售一个服务:一旦编译完成,SPA就是一个可以在任何地方免费托管的单个JS文件。但是,服务器端渲染的应用程序需要一个运行服务器。而服务器是可以销售的产品。也许阴谋论,但我没有看到其他破坏React生态系统的原因。
notion image
 
当然也有反对者:
我不认同这种说法,从我个人的观点来看,对服务器的需求仍然相对不变。我不认为这是Vercel的目标,而是React的长期目标是超越其替代品并改善用户体验。我这么说是因为我曾经与Vercel的销售团队有一段糟糕的经历,并且在2017-2018年积极地为Next.js做出了代码贡献。对我来说,根本的变化在于客户端不需要重新做服务器已经完成的工作。开发者仍然可以使用常规CDN将Next.js进行静态导出。然而,RSC提高了DX(开发者体验)和UX(用户体验)。
(对于静态导出可能没有显著改进,但我认为有所改善)。
这儿也有针对这篇文章的大量讨论
notion image
notion image

四、资料

 
rsc-parser
alvarlagerlofUpdated Jun 30, 2024
 
  • Next.js
  • Prisma初探小程序手机号验证码集采
    Loading...