banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

为什么是 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 呈现当前的时间。我们很快就写出了这样的代码。

import { useEffect, useState } from 'react'

export default function Home() {
  return <div>{Date.now()}</div>
}

由于水合时,浏览器的时间和服务器渲染时不同,导致数据不一致。就得到了水合错误。

Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

在 Next.js 14.2.x 以上版本,你可以更加明确的知道为什么出现这个问题。

image

由于传统 SSR 需要同构,数据水合就需要手动处理。例如上面的例子中,我们需要显示服务器时间。我们就需要使用 getServerSideProps 去确保数据的恒定。

import { useEffect, useState } from 'react'

export default function Home({ props: time }: { props: number }) {
  return <div>{time}</div>
}

export const getServerSideProps = async () => {
  return {
    props: Date.now(),
  }
}

这种分离式写法,如果在大量状态的情况下,将会变的非常难以管理,并且服务器数据的获取必须都集中在当前页面中,而不是在单个组件中,这让开发体验也会更加复杂。

例如,当你需要获取更多服务器数据时并且组件依赖服务器数据时,你需要把服务器数据从页面顶层传入到每个组件中,如果组件层级很深,你就不得不用 Context 或者状态库去传递了,即便组件逻辑和页面并没有强关联。这种方式限制了组件的复用,因为这类组件始终需要从页面顶层获取服务器数据,而不是独立的逻辑取得状态:

https://nextjs-book.innei.in/draw/10.page-server-props.json

那么,在 RSC 的模式下,我们容易把需要的数据和组件结合起来,例如上面的例子,我们可以很快的封装成组件。

```tsx filename="app/page.tsx" import { Servertime } from './components/server-time'

export default function Home() {
return
}

</Tab>

 <Tab label="app/components/server-time.tsx">
```tsx filename="app/components/server-time.tsx"
import { ServertimeClient } from './server-time.client'

export function Servertime() {
  return <ServertimeClient time={Date.now()} />
}
```tsx filename="app/components/server-time.client.tsx" 'use client'

import { useEffect, useState } from 'react'

export function ServertimeClient(props: { time: number }) {
const [currentTime, setCurrentTime] = useState(props.time)
useEffect(() => {
// 简单的时间处理,并不可靠。
const id = setInterval(() => {
setCurrentTime(time => time + 1000)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
return

{currentTime}

}

</Tab>
</Tabs>

上面的例子中,`ServerTime` 组件可以在任何 Server Component 中使用,并且无须传入 props。

```excalidraw
https://nextjs-book.innei.in/draw/11.app-server-props.json

更小的包体积#

由于 Server Component 只运行在服务端,那么在 Server Component 中使用到的外部库不会再浏览器端加载。这对于很多需要借助三方库去处理数据或者图表更加方便。一般的,这些库体积都会很大,同时这些数据可以仅在服务端处理完成。浏览器端少加载了 JS,既减轻了网络负载也加快了首屏性能。

下面是一个简单的例子。比如代码高亮,一般的我们借助 PrismShiki 等三方库去实现。而这类库体积一般都很大,如果需要导入所有语言,那么打包之后的体积可能会增加好几兆。

下文假设我们使用 Shiki 进行高亮代码。

一般的我们会将使用这类库的组件,使用 lazy 或者 dynamic 进行代码分割,防止在首屏加载庞大的 JS 文件降低 LCP 的指标。

const HighLighter = lazy(() =>
  import('./components/shiki').then((mod) => ({
    default: mod.HighLighter,
  })),
)

export default function () {
  return (
    <div>
      <Suspense fallback={'loading code block..'}>
        <HighLighter content='const foo = "bar";' lang="ts" />
      </Suspense>
    </div>
  )
}

但是,既然服务端返回的 HTML 中已经渲染好了高亮后的 DOM,浏览器还是需要下载 Shiki 再进行一遍高亮就很没有必要。

image

image

而使用 Server Component,这个组件的逻辑都在服务端完成,所以前端渲染此组件没有任何的逻辑,自然也不会去下载 Shiki 了。这样的话 Shiki 也就不会打包进 Client 的 JS undle 里去了。

import { bundledLanguages, getHighlighter } from 'shiki'
import type { FC } from 'react'
import type {
  BundledLanguage,
  BundledTheme,
  CodeToHastOptions,
  HighlighterCore,
} from 'shiki'

function codeHighlighter(
  highlighter: HighlighterCore,
  {
    lang,
    attrs,
    code,
  }: {
    lang: string
    attrs: string
    code: string
  },
) {
  const codeOptions: CodeToHastOptions<BundledLanguage, BundledTheme> = {
    lang,
    meta: {
      __raw: attrs,
    },
    themes: {
      light: 'github-light',
      dark: 'github-dark',
    },
  }

  return highlighter.codeToHtml(code, {
    ...codeOptions,
    transformers: [...(codeOptions.transformers || [])],
  })
}

export const HighLighter: FC<{
  lang: string
  content: string
}> = async (props) => {
  const { lang: language, content: value } = props

  const highlighter = await getHighlighter({
    themes: [
      import('shiki/themes/github-light.mjs'),
      import('shiki/themes/github-dark.mjs'),
    ],
    langs: Object.keys(bundledLanguages),
  })

  return (
    <div
      dangerouslySetInnerHTML={{
        __html: codeHighlighter(highlighter, {
          attrs: '',
          code: value,
          lang: language || '',
        }),
      }}
    />
  )
}

export default () => {
  return <HighLighter content='const foo = "bar"' lang="ts" />
}

效果是显著的。

image

此文由 Mix Space 同步更新至 xLog
原始链接为 https://innei.in/posts/tech/why-react-server-component-1


読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。