banner
innei

innei

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

なぜ RSC なのか (二)

漸進的レンダリング#

Important

漸進的レンダリング、またはストリーミングレンダリングとも呼ばれます。これは RSC でのみ享受できる特徴ではなく、このレンダリングモードは SuspenserenderToPipeableStream または renderToReadableStream に関連しています。

しかし、Next.js では App router RSC を使用する必要があります。この特徴を享受するためには。したがって、このセクションでは Next.js のレンダリングモードについて説明します。

RSC コンポーネントは非同期をサポートしているため、コンポーネント間のレンダリングは相互依存せず、分割可能です。複数のコンポーネントは、先に完了したものが先にレンダリングされます。これは、コンポーネント間で異なるデータを取得する際に非常に便利です。

例えば、あるページに A コンポーネントが商品リストを取得してレンダリング出力し、B コンポーネントが商品カテゴリを取得して出力する場合、両者は独立したロジックを持っています。

従来の SSR モードでは、ページ内のコンポーネントのデータはページの最上部から取得され、コンポーネントに渡される必要があるため、A および B コンポーネントのレンダリングは、ページデータの取得が完了するまで待機する必要があります。

私たちのインターフェースが次のようであると仮定します:

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const fetchGoods = async () => {
  await sleep(1000)
  return [
    {
      name: 'iPhone 15',
      variants: ['Blue'],
    },
    {
      name: 'iPad Pro',
      variant: 'Black',
    },
  ]
}
const fetchCategories = async () => {
  await sleep(3000)
  return ['Electronics', 'Books']
}
export default (props: {
  goods: { name: string; variants: string[] }[]
  categories: string[]
}) => {
  return (
    <div>
      <h1>商品</h1>
      <ul>
        {props.goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
      <h1>カテゴリ</h1>
      <ul>
        {props.categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

export const getServerSideProps = async () => {
  const [goods, categories] = await Promise.all([
    fetchGoods(),
    fetchCategories(),
  ])
  return {
    props: {
      goods,
      categories,
    },
  }
}

上記の例では、サーバーがブラウザに応答するのに少なくとも 3 秒かかり、その後ブラウザでデータが表示されます。

RSC では、両者の間で先に完了したものが先にレンダリングされます。


export default () => {
  return (
    <>
      <Suspense>
        <Goods />
      </Suspense>

      <hr className="my-8 h-1 bg-gray-100" />
      <Suspense>
        <Categories />
      </Suspense>
    </>
  )
}

const Goods = async () => {
  const goods = await fetchGoods()
  return (
    <div>
      <h1>商品</h1>
      <ul>
        {goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
    </div>
  )
}
const Categories = async () => {
  const categories = await fetchCategories()
  return (
    <div>
      <h1>カテゴリ</h1>
      <ul>
        {categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

image

1 秒待った後、最初に Goods がレンダリングされ、2 秒後に Categories がレンダリングされることがわかります。これが漸進的レンダリングの利点であり、First Meaningful Paint (FMP) と Largest Contentful Paint (LCP) を最大限に向上させます。

Important

このレンダリング方式は首屏のパフォーマンスを向上させますが、この特徴によりページレイアウトの揺れがより顕著になるため、開発中はこの点に特に注意が必要です。Supense fallback に元のコンポーネントと同じサイズのプレースホルダーを埋めるようにしてください。

柔軟なサーバーデータ取得#

RSC では任意の Nodejs メソッドを使用できるため、データ取得が非常に便利です。別途サーバー API を作成し、クライアントから API をリクエストする必要はなく、SSR でインターフェースをリクエストしてデータをクライアントコンポーネントに水和する必要もありません。サーバーからデータを取得するメソッドを作成し、RSC で直接呼び出すだけです。

例えば、サーバーの管理を行う場合、コンポーネントがサービスの状態情報を必要とします。

RSC の前では、一般的に次のようにサーバーの状態情報を取得していました。

まず、SSR 時に getServerSideProps を使用して getServerStatus() を呼び出し、データを返し、ページでこの props を受け取ります。状態を定期的に更新する必要がある場合は、このメソッドをラップする API インターフェースを作成し、RCC でポーリングする必要があります。

https://nextjs-book.innei.in/draw/12.server-status-in-ssr-and-csr.json

RSC では、直接呼び出してレンダリングし、revalidatePath() を使用してデータを更新できます。API を作成する必要はありません。

export default function Page() {
  return (
    <div className="flex gap-4">
      <ServerStatus />
      <Revalidate />
    </div>
  )
}

const ServerStatus = async () => {
  // サーバーの状態にアクセス
  const status = await getServerStatus()

  return <>{/* レンダー */}</>
}
'use server'
export const revalidateStatus = async () => {
  revalidatePath('/status')
}

'use client'

export const Revalidate = () => {
  useEffect(() => {
    const timerId = setInterval(() => {
        revalidateStatus()
      }, 5000)
    return () => {
      clearInterval(timerId)
    }
  }, [])
  return null
}

ここでの Revalidate コンポーネント + revalidateStatus は、サーバーアクションの特徴を利用してページのデータ更新を行っています。

見た目には、ここで 3 つのファイルを書く必要があり、RSC と RCC を区別するのは複雑に見えますが、API を別途作成し、手動で API の型定義を書く必要がなく、End-to-End 型安全を実現できる分、はるかに良いです。

サーバーアクションの利点#

前のセクションでは、サーバーアクションを利用してページのデータ更新を行いましたが、実際にはサーバーアクションには他の用途もあります。

サーバーアクションは実際には POST リクエストであり、非同期のメソッドを作成し、'use server' とマークすると、RCC でこのメソッドを呼び出す際に、サーバーに POST リクエストを送信してこのメソッドの応答データを自動的に取得します。このデータはストリーミング可能であり、このメソッド内で revalidate などのメソッドを呼び出してページの更新をトリガーできます。

ドキュメントでは、サーバーアクションを使用してフォームの送信を処理し、データの更新をトリガーし、最終的に UI に反映させることが一般的に説明されています。

さらに、実際にはサーバーアクションを使用してデータを取得することもできます。

上記の例を再利用しますが、今回はすべて RCC 内でデータ取得とポーリングを実装します。

```tsx filename="app/server-action/page.tsx" {5,8} 'use client'

import useSWR from 'swr'

import { getLoadAvg } from './action'

export default function Page() {
const { data } = useSWR('get-load-avg', getLoadAvg, {
refreshInterval: 1000,
})
return <> 負荷平均: {data?.loadavg.join (', ')}</>
}

</Tab>
  <Tab label="app/server-action/action.tsx">
```tsx filename="app/server-action/action.tsx" {1}
'use server'

import os from 'os'

export const getLoadAvg = async () => {
  const avg_load = os.loadavg()

  return {
    loadavg: avg_load,
  }
}

このように、API インターフェースを一つ減らすことができ、またこの書き方は End-to-End 型安全を実現しています。

さらに、推奨される読み物:サーバーアクション & ストリーミング UI


なぜ RSC なのか - 異なる Next.js の話

この記事は Mix Space によって xLog に同期更新されました
元のリンクは https://innei.in/posts/tech/why-react-server-component-2


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