为什么是 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 以上版本,你可以更加明确的知道为什么出现这个问题。
![](https://object.innei.in/bed/2024/0419_1713519966563.png)
由于传统 SSR 需要同构,数据水合就需要手动处理。例如上面的例子中,我们需要显示服务器时间。我们就需要使用 getServerSideProps
去确保数据的恒定。
这种分离式写法,如果在大量状态的情况下,将会变的非常难以管理,并且服务器数据的获取必须都集中在当前页面中,而不是在单个组件中,这让开发体验也会更加复杂。
例如,当你需要获取更多服务器数据时并且组件依赖服务器数据时,你需要把服务器数据从页面顶层传入到每个组件中,如果组件层级很深,你就不得不用 Context 或者状态库去传递了,即便组件逻辑和页面并没有强关联。这种方式限制了组件的复用,因为这类组件始终需要从页面顶层获取服务器数据,而不是独立的逻辑取得状态:
那么,在 RSC 的模式下,我们容易把需要的数据和组件结合起来,例如上面的例子,我们可以很快的封装成组件。
上面的例子中,ServerTime
组件可以在任何 Server Component 中使用,并且无须传入 props。
更小的包体积
由于 Server Component 只运行在服务端,那么在 Server Component 中使用到的外部库不会再浏览器端加载。这对于很多需要借助三方库去处理数据或者图表更加方便。一般的,这些库体积都会很大,同时这些数据可以仅在服务端处理完成。浏览器端少加载了 JS,既减轻了网络负载也加快了首屏性能。
下面是一个简单的例子。比如代码高亮,一般的我们借助 Prism、Shiki 等三方库去实现。而这类库体积一般都很大,如果需要导入所有语言,那么打包之后的体积可能会增加好几兆。
下文假设我们使用 Shiki 进行高亮代码。
一般的我们会将使用这类库的组件,使用 lazy
或者 dynamic
进行代码分割,防止在首屏加载庞大的 JS 文件降低 LCP 的指标。
但是,既然服务端返回的 HTML 中已经渲染好了高亮后的 DOM,浏览器还是需要下载 Shiki 再进行一遍高亮就很没有必要。
![](https://object.innei.in/bed/2024/0419_1713536181295.png)
![](https://object.innei.in/bed/2024/0419_1713536382200.png)
而使用 Server Component,这个组件的逻辑都在服务端完成,所以前端渲染此组件没有任何的逻辑,自然也不会去下载 Shiki 了。这样的话 Shiki 也就不会打包进 Client 的 JS undle 里去了。
效果是显著的。
![](https://object.innei.in/bed/2024/0419_1713536803659.png)
渐进式渲染
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 发表你的观点吧。