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

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

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