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 バンドルに含まれなくなります。
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