JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第08章—渲染篇ReactServerComponent与SSR

首次发表于 2024-03-22, 更新于 2024-03-22

前言

Next.js v13 推出了基于 React Server Component 的 App Router 路由解决方案。对于 Next.js 而言堪称是一个颠覆式的更新,更是将 React 一直宣传的 React Server Component 这个概念真正推进并落实到项目中。

因为 React Server Component 的引入,Next.js 中的组件开始区分客户端组件还是服务端组件,但考虑到部分同学对 React Server Component 并不熟悉,本篇我们会先从 React Server Components 的出现背景开始讲起,并将其与常混淆的 SSR 概念做区分,为大家理解和使用服务端组件和客户端组件打下基础。

React Server Components

2020 年 12 月 21 日,React 官方发布了 React Server Components 的[介绍文章](Introducing Zero-Bundle-Size React Server Components – React Blog),并配上了由 Dan Abramov 和 Lauren Tan 两位 React 团队的工程师分享的长约 1h 的分享Demo,详细的介绍了 React Server Components 的出现背景和使用方式。

了解 React Server Components 对理解 Next.js 的渲染方式至关重要。所以我们稍微花些篇幅来回顾下这场演讲的主要内容。

其中 Dan 介绍了应用开发的三个注意要点:

Data Fetching with React Server Components - YouTube - 1_41.png

这三点分别是好的用户体验易于维护高性能。但是这三点却很难兼顾,我们以 Spotify 这个网站的页面为例:

Data Fetching with React Server Components - YouTube - 1_57.png

这是一个音乐家介绍页面,内容主要包含两块区域,一块是热门单曲区域(TopTracks),一块是唱片目录(Discography),如果我们要模拟实现这样一个页面,使用 React,我们可能会这样写:

Data Fetching with React Server Components - YouTube - 2_32.png

看起来很简洁的样子,但当我们加上数据请求后,代码就会变成这个样子:

Data Fetching with React Server Components - YouTube - 4_10.png

我们从顶层获取数据,然后传给需要的子组件,虽然一次请求就可以解决,但这样的代码并不易于维护。

比如在以后的迭代中删除了某个 UI 组件,但是对应数据没有从接口中删除,这就造成了冗余的数据。又比如你在接口里添加了一个字段,然后在某个组件里使用,但你忘记在另一个引用该组件的组件中传入这个字段,这可能就导致了错误。

为了易于维护,我们就会想回归到刚才简单的结构中,然后每个组件负责各自的数据请求:

Data Fetching with React Server Components - YouTube - 5_16.png

但是这样就慢了,本来一个请求就能解决,现在拆分为了三个请求。难道就不能全兼顾吗?

我们分析下原因,将数据请求拆分到各个组件中为什么会慢呢?本质上还是客户端发起了多次 HTTP 请求,如果这些请求是串行的(比如 TopTracks 和 Discography 组件需要在 ArtistDetails 组件的数据返回后再拿其中的 id 数据发送请求),那就更慢了。为了解决这个问题,便有了 React Server Component。

Data Fetching with React Server Components - YouTube - 11_08.png

React Server Component 把数据请求的部分放在服务端,由服务端直接给客户端返回带数据的组件。

最终的目标是:在原始只有 Client Components 的情况下,一个 React 树的结构如下:

Data Fetching with React Server Components - YouTube - 48_03.png

在使用 React Server Component 后,React 树会变成:

Data Fetching with React Server Components - YouTube - 46_49.png

其中黄色节点表示 React Server Component。在服务端,React 会将其渲染会一个包含基础 HTML 标签和客户端组件占位的树。它的结构类似于:

image.png

因为客户端组件的数据和结构在客户端渲染的时候才知道,所以客户端组件此时在树中使用特殊的占位进行替代。

当然这个树不可能直接就发给客户端,React 会做序列化处理,客户端收到后会在客户端根据这个数据重构 React 树,然后用真正的客户端组件填充占位,渲染最终的结果。

image.png

使用 React Server Component,因为服务端组件的代码不会打包到客户端代码中,它可以减小包(bundle)的大小。且在 React Server Component 中,可以直接访问后端资源。当然因为在服务端运行,对应也有一些限制,比如不能使用 useEffect 和客户端事件等。

在这场分享里,Dan 也提到了 Next.js,表示会和 Next.js 团队的合作伙伴们一起开发,让每个人都能使用这个功能。

Data Fetching with React Server Components - YouTube - 42_56.png

终于 Next.js 在 v13 版本中实现了 React Server Component,此时已过去了两年之久。

Server-side Rendering

Server-side Rendering,中文译为“服务端渲染”,在上篇《渲染篇 | 从 CSR、SSR、SSG、ISR 开始说起》已经介绍过,并提供了一个基于 Pages Router 的 demo:

// pages/ssr.js
export default function Page({ data }) {
  return <p>{JSON.stringify(data)}</p>
}
 
export async function getServerSideProps() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/todos`)
  const data = await res.json()
 
  return { props: { data } }
}

从这个例子中可以看出,Next.js v12 之前的 SSR 都是通过 getServerSideProps这样的方法,在页面层级获取数据,然后通过 props 传给每个组件,然后将整个组件树在服务端渲染为 HTML。

但是 HTML 是没有交互性的(non-interactive UI),客户端渲染出 HTML 后,还要等待 JavaScript 完全下载并执行。JavaScript 会赋予 HTML 交互性,这个阶段被称为水合(Hydration)。此时内容变为可交互的(interactive UI)。

从这个过程中,我们可以看出 SSR 的几个缺点:

  1. SSR 的数据获取必须在组件渲染之前
  2. 组件的 JavaScript 必须先加载到客户端,才能开始水合
  3. 所有组件必须先水合,然后才能跟其中任意一个组件交互

可以看出 SSR 这种技术“大开大合”,加载整个页面的数据,加载整个页面的 JavaScript,水合整个页面,还必须按此顺序串行执行。如果有某些部分慢了,都会导致整体效率降低。

此外,SSR 只用于页面的初始化加载,对于后续的交互、页面更新、数据更改,SSR 并无作用。

RSC 与 SSR

了解了这两个基本概念,现在让我们来回顾下 React Server Components 和 Server-side Rendering,表面上看,RSC 和 SSR 非常相似,都发生在服务端,都涉及到渲染,目的都是更快的呈现内容。但实际上,这两个技术概念是相互独立的。RSC 和 SSR 既可以各自单独使用,又可以搭配在一起使用(搭配在一起使用的时候是互补的)。

正如它们的名字所表明的那样,Server-side Rendering 的重点在于 Rendering,React Server Components 的重点在于 Components

简单来说,RSC 提供了更细粒度的组件渲染方式,可以在组件中直接获取数据,而非像 Next.js v12 中的 SSR 顶层获取数据。RSC 在服务端进行渲染,组件依赖的代码不会打包到 bundle 中,而 SSR 需要将组件的所有依赖都打包到 bundle 中。

当然两者最大的区别是:

SSR 是在服务端将组件渲染成 HTML 发送给客户端,而 RSC 是将组件渲染成一种特殊的格式,我们称之为 RSC Payload。这个 RSC Payload 的渲染是在服务端,但不会一开始就返回给客户端,而是在客户端请求相关组件的时候才返回给客户端,RSC Payload 会包含组件渲染后的数据和样式,客户端收到 RSC Payload 后会重建 React 树,修改页面 DOM。

这样的描述好像很抽象,其实很简单。让我们本地开启一下当时 React 提供的 Server Components Demo

image.png

你会发现 localhost 这个 HTML 页面的内容就跟 CSR 一样,都只有一个用于挂载的 DOM 节点。当点击左侧 Notes 列表的时候,会发送请求,这个请求的地址是:

http://localhost:4000/react?location={"selectedId":3,"isEditing":false,"searchText":""}

返回的结果是:

image.png

除此之外没有其他的请求了。其实这条请求返回的数据就是 RSC Payload。

让我们看下这条请求,我们请求的这条笔记的标题是 Make a thing,具体内容是 It's very easy to make some……,我们把返回的数据具体查看一下,你会发现,返回的请求里包含了这些数据:

截屏2024-03-04 16.51.55.png

不仅包含数据,完整渲染后的 DOM 结构也都包含了。客户端收到 RSC Payload 后就会根据这其中的内容修改 DOM。而且在这个过程,页面不会刷新,页面实现了 partial rendering(部分更新)。

这也就带来了我们常说的 SSR 和 RSC 的最大区别,那就是状态的保持(渲染成不同的格式是“因”,状态的保持是“果”)。每次 SSR 都是一个新的 HTML 页面,所以状态不会保持(传统的做法是 SSR 初次渲染,然后 CSR 更新,这种情况,状态可以保持,不过现在讨论的是 SSR,对于两次 SSR,状态是无法维持的)。

但是 RSC 不同,RSC 会被渲染成一种特殊的格式(RSC Payload),可以多次重新获取,然后客户端根据这个特殊格式更新 UI,而不会丢失客户端状态。

所谓不丢失状态,让我们看个例子:

1116.gif

在上图中,我们新建了一条 note,重点在左侧的搜索结果列表,新建后,原本的那条 note 依然保持了展开状态。(注:这个状态在技术上是通过 useState 来实现的。实战篇的时候会用 Next.js 重写这个 Demo)

注意:这里我们比较的是 React Demo 展示的 RSC 特性和 Next.js v12 所代表的传统 SSR。跟我们接下来要讲的 Next.js 服务端组件、客户端组件并不一样。

Next.js 的服务端组件、客户端组件虽然是基于 RSC 提出的用于区分组件类型的概念,但在具体实现上,为了追求高性能,技术上其实是融合了 RSC 和 SSR(前面也说过,RSC 和 SSR 互补)。这里比较是纯粹的 RSC 和 SSR,以防大家在概念理解上产生混淆。

总结

本篇我们介绍并比较了 RSC 和 SSR,虽然并不涉及 Next.js 具体的写法和使用,但对于大家理解 Next.js 中的服务端组件、客户端组件概念有所帮助。

© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru