为什么是 RSC
React 19 会正式引入 React Server Component(RSC) 的概念,Client Component 和 Server Component 从此将会正式分离。Next.js 从 13 版本就开始支持 Server Component。那么为什么是 RSC?优势到底何在?这一章节我们来探讨一下这个问题。
规避水合错误
RSC 的出现减少了 水合错误 (Hydration Error) 的发生,如果你只使用 Server Component 去描述所有的组件的,那么水合错误也不会发生。
首先我们来复习一下,为什么会出现水合错误。
我们知道在传统 SSR 架构中,代码是同构的,即页面渲染前服务器需要渲染一遍并返回 HTML 给到浏览器做一遍静态渲染,等待 JS 加载完成后,浏览器在执行 JS 代码重新运行这段代码,将状态和事件交互绑定到 UI 上。如果这一步的状态和服务器渲染时状态不一致,那么就会出现水合错误。
我们来看一个简单的例子 -- 显示当前的服务器时间。假设我们需要 UI 呈现当前的时间。我们很快就写出了这样的代码。
由于水合时,浏览器的时间和服务器渲染时不同,导致数据不一致。就得到了水合错误。
在 Next.js 14.2.x 以上版本,你可以更加明确的知道为什么出现这个问题。
由于传统 SSR 需要同构,数据水合就需要手动处理。例如上面的例子中,我们需要显示服务器时间。我们就需要使用 getServerSideProps
去确保数据的恒定。
这种分离式写法,如果在大量状态的情况下,将会变的非常难以管理,并且服务器数据的获取必须都集中在当前页面中,而不是在单个组件中,这让开发体验也会更加复杂。
例如,当你需要获取更多服务器数据时并且组件依赖服务器数据时,你需要把服务器数据从页面顶层传入到每个组件中,如果组件层级很深,你就不得不用 Context 或者状态库去传递了,即便组件逻辑和页面并没有强关联。这种方式限制了组件的复用,因为这类组件始终需要从页面顶层获取服务器数据,而不是独立的逻辑取得状态:
那么,在 RSC 的模式下,我们容易把需要的数据和组件结合起来,例如上面的例子,我们可以很快的封装成组件。
上面的例子中,ServerTime
组件可以在任何 Server Component 中使用,并且无须传入 props。
更小的包体积
由于 Server Component 只运行在服务端,那么在 Server Component 中使用到的外部库不会再浏览器端加载。这对于很多需要借助三方库去处理数据或者图表更加方便。一般的,这些库体积都会很大,同时这些数据可以仅在服务端处理完成。浏览器端少加载了 JS,既减轻了网络负载也加快了首屏性能。
下面是一个简单的例子。比如代码高亮,一般的我们借助 Prism、Shiki 等三方库去实现。而这类库体积一般都很大,如果需要导入所有语言,那么打包之后的体积可能会增加好几兆。
下文假设我们使用 Shiki 进行高亮代码。
一般的我们会将使用这类库的组件,使用 lazy
或者 dynamic
进行代码分割,防止在首屏加载庞大的 JS 文件降低 LCP 的指标。
但是,既然服务端返回的 HTML 中已经渲染好了高亮后的 DOM,浏览器还是需要下载 Shiki 再进行一遍高亮就很没有必要。
而使用 Server Component,这个组件的逻辑都在服务端完成,所以前端渲染此组件没有任何的逻辑,自然也不会去下载 Shiki 了。这样的话 Shiki 也就不会打包进 Client 的 JS undle 里去了。
效果是显著的。
渐进式渲染
Important渐进式渲染,或者称作流式渲染。这不是一个只能在 RSC 中可以享受到的特征,这种渲染模式和
Suspense
、renderToPipeableStream
或renderToReadableStream
有关。但是在 Next.js 中你需要使用 App router RSC 才能享受此特征。
由于 RSC 组件支持异步,所以组件和组件平行关系之间的渲染并没有相互依赖性,并且可被拆分。多个组件可以谁先兑现谁先渲染。这在组件之间分别获取不同数据时非常好用。
例如一个页面上,存在两个组件,A 组件获取商品列表并渲染输出,B 组件获取商品分类并输出。两者都是独立的逻辑。
在传统 SSR 模式中,页面中组件的数据需要从页面顶层获取向下传递到组件,这样就会导致 A,B 组件的渲染都要等待页面数据获取完才能喜欢渲染。
假设我们的接口为:
上面的例子中,服务器响应浏览器至少需要 3s,之后才能在浏览器呈现数据。
如果在 RSC 中,两者之间可以谁先完成谁先渲染。
可以看到,等待 1s 后首先渲染出了 Goods,然后 2s 之后渲染出 Categories。这便是渐进式渲染的好处,最大程度提升了 First Meaningful Paint (FMP) 和 Largest Contentful Paint (LCP)。
Important这种渲染方式虽然提升了首屏的性能,但是因为这个特征也会让页面布局的抖动更加明显,在开发过程中应该更加需要注意这点,尽量在 Supsense fallback 中填充一个和原始组件大小相同的占位。
灵活的服务器数据获取
由于 RSC 中可以使用任何 Nodejs 方法,所以在数据获取上异常方便,我们不必单独编写一个 Server Api,然后只在 Client 去请求 API,也不必在 SSR 中请求接口,把数据水合到 Client 组件中。我们只需要编写到服务端获取数据的方法,然后直接在 RSC 中调用。
例如,我们现在做一个服务器的管理,其中有组件需要服务的状态信息。
在 RSC 之前我们一般这样去获取服务器的状态信息。
首先,在 SSR 时,使用 getServerSideProps 调用 getServerStatus()
把数据返回,然后在 Page 中接收这个 props。如果需要定时去刷新这个状态的话,我们还需要编写一个 API 接口包装这个方法,在 RCC 中轮询。
在 RSC 中,我们直接调用并渲染,然后使用 revalidatePath()
去做数据刷新,无需编写任何 API。
这里的 Revalidate 组件 + revalidateStatus
就是利用了 Server Action 的特征去做了页面的数据更新。
看似这里需要写三个文件,又要区分 RSC 和 RCC 好像挺复杂的,但是比起另写 API 和还有手动写 API 的类型定义并且无法做到 End-to-End type safe 的割裂感还是好太多了。
Server Action 的优势
上一节其实已经利用了 Server Action 完成了页面的数据更新,其实 Server Action 还有其他的用法。
我们知道 Server Action 其实一个 POST 请求,我们编写一个异步的方法,并且标记为 'use server',那么在 RCC 中调用这个方法时,会自动帮你完成:向服务器发送 POST 请求获取这个方法的响应数据,这个数据可以是流式的,并且在此方法中可以调用 revalidate
等方法去触发页面的更新。
文档中一般会告诉你 Server action 来处理表单的提交,触发对数据的更新,最后反应到 UI 上。
不仅如此,其实我们可以利用 Server Action 去获取数据。
还是以上面的例子为例,只不过这次我们全部在 RCC 中实现数据获取和轮询。
这样不仅少写了一个 API 接口,同时这样的写法也做到了 End-to-End type safe。
另外,推荐阅读:Server Action & Streamable UI
最后更新于 2024/7/26 16:15:59
本书还在编写中..
前往 https://innei.in/posts/tech/my-first-nextjs-book-here#comment 发表你的观点吧。