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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。