漸進的レンダリング#
Important
漸進的レンダリング、またはストリーミングレンダリングとも呼ばれます。これは RSC でのみ享受できる特徴ではなく、このレンダリングモードは Suspense
、 renderToPipeableStream
または 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>
)
}
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
この記事は Mix Space によって xLog に同期更新されました
元のリンクは https://innei.in/posts/tech/why-react-server-component-2