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 以上版本,你可以更加明確的知道為什麼出現這個問題。
由於傳統 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()} />
}
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
}
</Tab>
</Tabs>
上面的例子中,`ServerTime` 組件可以在任何 Server Component 中使用,並且無須傳入 props。
```excalidraw
https://nextjs-book.innei.in/draw/11.app-server-props.json
更小的包體積#
由於 Server Component 只運行在伺服器端,那麼在 Server Component 中使用到的外部庫不會再瀏覽器端加載。這對於很多需要借助三方庫去處理數據或者圖表更加方便。一般的,這些庫體積都會很大,同時這些數據可以僅在伺服器端處理完成。瀏覽器端少加載了 JS,既減輕了網絡負載也加快了首屏性能。
下面是一個簡單的例子。比如代碼高亮,一般的我們借助 Prism、Shiki 等三方庫去實現。而這類庫體積一般都很大,如果需要導入所有語言,那麼打包之後的體積可能會增加好幾兆。
下文假設我們使用 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 再進行一遍高亮就很沒有必要。
而使用 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" />
}
效果是顯著的。
此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://innei.in/posts/tech/why-react-server-component-1