Blog.

该如何理解 React RSC

LiuuY

React RSC 已经推出一段时间,相对于常见的 React 的渲染模式(SPA、SSR/SSG)等等有很大的不同,它将数据获取、服务端/浏览器端渲染融合在一起,减少了包大小、提升了渲染性能。但同时也增加了 React 的复杂度,甚至截止本文发布时只有 Next.js 支持 RSC, 所以本文从原理的角度介绍 RSC,帮助大家更好的理解。

两种组件、三种渲染模式

在 RSC 出现之前,我们只有「一种组件」和「两种渲染模式」,「一种组件」指所有的组件没有本质的区别,都是一个包含标签(HTML)逻辑(JavaScript)和样式(CSS)的模块,甚至可以说组件就是一个函数:

function Greeting({ name }) {
  return (
    <h1 className="greeting">
      Hello <i>{name}</i>. Welcome!
    </h1>
  );
}

JSX 仅仅是创建标签的语法糖,上面的代码也就是:

import { createElement } from 'react';

function Greeting({ name }) {
  return createElement(
    'h1',
    { className: 'greeting' },
    'Hello ',
    createElement('i', null, name),
    '. Welcome!'
  );
}

而「两种渲染模式」指的就是,渲染组件的方式,包括:Client Side Render(CSR)Server Side Render(SSR)(包含 Server Side Generate(SSG)),即组件可以在浏览器中渲染成页面可见的 HTML 标签,或者在服务器端预先渲染成 HTML 标签然后发送到浏览器中显示。同时,我们知道 SSR/SSG 只是针对第一加载页面的优化,后续组件的行为还是会在浏览器中渲染(即 Hydrate)。

但在 RSC 的出现后,我们现在有了「两种组件」:Server Components 和 Client Components,「三种渲染模式」:CSR、SSR/SSG 和 Server Components Render

Server Components

Server Components 是一种在服务器端渲染的组件,由于不会在浏览器环境渲染,它不能有用户交互相关的逻辑(onClickuseState 等等)。原先我们熟悉的组件则称为 Client Components。

Client Components 中的「Client」有些误导,它指的并不是组件只能在「Client」(一般就是浏览器)中渲染,也可以使用 SSR/SSG 渲染。

Client Components 与 Server Components 的核心区别就是,Client Components 无论是否 SSR/SSG 渲染(也仅仅针对第一次请求),后续渲染都需要在浏览器中由 React 完成,而 Server Components 永远不会在浏览器渲染,包括数据请求也在服务端时候完成,而且 Server Components 的刷新(Rerender)也是通过路由刷新,重新在服务器端完成,然后在发送给浏览器,如果所有数据请求都在 Server Components 完成,浏览器就不会发一个数据请求🤯。(对于静态内容也可以在构建时候完成

一种简单的理解是:我们熟悉的「容器/展示模式」(Container/Presentational Pattern),Server Components 就是将容器组件放到了服务器端渲染,浏览器得到的并不是组件而是已经渲染好的 HTML 标签(严格来说浏览器得到并不是 HTML 标签,这需要经过 SSR/SSG 才可以,后面会讲到),当然容器组件和 Server Components 并不完全一致,仅仅是作为易于理解的参考。

使用 Server Components 将用户交互逻辑(使用 Client Components)与可以不在浏览器中完成的逻辑分离,首先可以减少发送到浏览器代码量,比如这个 Server Component:

import { parseISO, format } from 'date-fns'

async function DateDisplay() {
  const dateString = await getCurrentDate()
  const date = parseISO(dateString)

  return <time dateTime={dateString}>{format(date, 'yyyy-MM-dd')}</time>
}

由于这个组件在服务器端渲染,浏览器得到的只是 <time datetime="2008-02-14">2008-02-14</time>,并不会包含 date-fns 这个库,这样便可以显著的减少发送到浏览器的包大小。

同时,由于服务器端并没有并发请求限制而且离数据源更近,使得请求速度快,渲染更快。

Client Components

Client Components 是指渲染在客户端的组件(客户端指浏览器,也可以被 SSR/SSG 等)。它需要在文件最上面加上 'use client' 用于标识本文件中的组件是 Client Components。

一个组件如果没有被标识为 'use client',那它如果在一个 Client Component 中渲染,则它就是 Client Components,反之如果在一个 Server Components 中渲染,则它就是 Server Components。

Server Components Render

Server Components 是如何渲染的呢?很简单,由服务器端将组件渲染完后,再序列化(Serialization)发送到浏览器,序列化后的格式被称为:RSC Payload (有时候也被称为:RSC Wire Format 或 RSC Flight Format)。

例如:

// ClientComponent.jsx
"use client";

export default function ClientComponent({ children }) {
  return (
    <div>
      <button onClick={() => alert("hello")}>Show</button>
      {children}
    </div>
  );
}


// OuterServerComponent.jsx
import ClientComponent from './ClientComponent'
import ServerComponent from './ServerComponent'

export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <DateDisplay />
    </ClientComponent>
  )
}

对于 Server Components 可以转化成(实际格式更为复杂):

// <DateDisplay />
{
  $$typeof: Symbol(react.element),
  type: "time",
  props: {
    dateTime: '2008-02-14',
    children: "2008-02-14"
  }
}

由于 Client Components 中的可能存在事件处理函数、Promise 等等无法序列化,RSC Wire Format 采用了记录 Client Components 的「模块的引用」的方式,包括组件所在的文件位置、名字等等:

// <ClientComponent />
{
  $$typeof: Symbol(react.element),
  type: {
    $$typeof: Symbol(react.module.reference),
    name: "default",
    filename: "./src/ClientComponent.client.js"
  }
}

这样,在真正需要渲染成 HTML 的时候(CSR、SSR/SSG),就可以通过这些「模块的引用」找到真正的代码并执行。

总结一下,Server Component Render 即是将组件树中的 Server Components 在服务器端渲染;对于 Client Components 则记录其所在模块,然后序列化为 RSC Wire Format,供后续的 SSR/SSG 或者 CSR 渲染使用。

同时由于序列化也带来了一些限制,例如 Server Components 向 Client Components 传递的 Props 必须是可以序列化的,否则在后续变无法将渲染的内容序列化:

// OuterServerComponent.tsx
import ClientComponent from './ClientComponent'

export default function OuterServerComponent() {
  const callback = () => alert('call from client') // ❌ NOT Serializable

  return <ClientComponent callback={callback}>
}

但是由于 Server Components 本身是可以序列化的,所以可以将 Server Components 作为 Props 传给 Client Components,这也就是为什么 Client Components 可以包含(作为 children)的原因。

// OuterServerComponent.tsx
import ClientComponent from './ClientComponent'
import ServerComponent from './ServerComponent'

export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

那 Server Components Render 对 SSR/SSG 的影响是什么呢?答案是:没有影响。从上面的描述,我们也能看出,SSR/SSG 都是后续针对 RSC Wire Format 的渲染流程,有无 Server Components,SSR/SSG 都仅仅是对于用户第一请求将 RSC Wire Format 渲染为 HTML 标签后发送到浏览器。

可以将这个流程看作一个流水线:

总结

RSC 增加了一种新的组件形式:Server Components,一种在服务器端渲染的组件,它的刷新需要结合路有操作。Server Components 可以显著减小发送浏览器包的大小,以及数据请求的性能。

RSC 的渲染逻辑是:组件树在服务器端渲染并序列化成为 RSC Wire Format 后,再进行 SSR/SSG 成为 HTML 或者发送给浏览器供 React 直接使用。