一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • Server Componentsの深層理解:RSCストリーミングの仕組みと活用

React Server Componentsの本質とは

React Server Components(RSC)は、Reactの歴史において最も大きなパラダイムシフトの一つです。コンポーネントをサーバー側で実行し、そのレンダリング結果をストリーミングでクライアントに送信するこのアーキテクチャは、従来のCSR(Client-Side Rendering)やSSR(Server-Side Rendering)とは根本的に異なるアプローチを取っています。

本記事では、RSCの内部的な仕組み、特にストリーミングプロトコルの動作原理を深掘りし、実践的な活用パターンを解説します。Next.js App Routerの裏側で何が起きているのかを理解することで、より効果的なアプリケーション設計が可能になります。

RSCストリーミングプロトコルの仕組み

RSCのストリーミングは、独自のワイヤーフォーマットを使用してサーバーからクライアントにデータを送信します。このフォーマットは、コンポーネントツリーをシリアライズ可能な形式に変換したものです。

サーバーコンポーネントがレンダリングされると、その結果はReact Flightプロトコルと呼ばれる形式でエンコードされます。これは、JSONに似た独自のストリーミング形式で、以下の特徴を持ちます。

  • コンポーネントの出力を段階的に送信可能(チャンク単位)
  • クライアントコンポーネントへの参照をモジュール識別子として保持
  • Suspenseバウンダリに対応した非同期チャンクの解決

Server ComponentとClient Componentの境界

RSCアーキテクチャにおいて最も理解すべきは、サーバーコンポーネントとクライアントコンポーネントの境界です。

// app/posts/page.tsx(Server Component)
import { PostList } from './post-list'
import { db } from '@/lib/database'

export default async function PostsPage() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
})

return (
<div>
<h2>最新記事</h2>
<PostList initialPosts={posts} />
</div>
)
}

// app/posts/post-list.tsx(Client Component)
'use client'

import { useState } from 'react'

export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [filter, setFilter] = useState('')

const filteredPosts = posts.filter(post =>
post.title.toLowerCase().includes(filter.toLowerCase())
)

return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="記事を検索..."
/>
<ul>
{filteredPosts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}

この例では、データベースアクセスはサーバーコンポーネントで行い、インタラクティブなフィルタリング機能はクライアントコンポーネントに委譲しています。

Suspenseを活用したストリーミングパターン

RSCの真価は、Suspenseとの組み合わせで発揮されます。重い処理を含むコンポーネントをSuspenseでラップすることで、ページの他の部分を先に表示し、重い部分を後からストリーミングで配信できます。

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { UserProfile } from './user-profile'
import { ActivityFeed } from './activity-feed'
import { AnalyticsChart } from './analytics-chart'

export default function DashboardPage() {
return (
<div>
<Suspense fallback={<p>プロフィール読み込み中...</p>}>
<UserProfile />
</Suspense>

<Suspense fallback={<p>アクティビティ読み込み中...</p>}>
<ActivityFeed />
</Suspense>

<Suspense fallback={<p>分析データ読み込み中...</p>}>
<AnalyticsChart />
</Suspense>
</div>
)
}

各Suspenseバウンダリは独立してストリーミングされるため、UserProfileが先に解決されればその部分だけ先に表示されます。

Server Actionsとの連携

Server Actionsは、RSCと対になる機能で、クライアントからサーバーの関数を直接呼び出すことを可能にします。

// app/actions/post.ts
'use server'

import { revalidatePath } from 'next/cache'
import { db } from '@/lib/database'

export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string

if (!title || title.length < 3) {
return { error: 'タイトルは3文字以上で入力してください' }
}

await db.post.create({
data: { title, content },
})

revalidatePath('/posts')
return { success: true }
}

revalidatePathを呼ぶことで、関連するサーバーコンポーネントが再実行され、最新のデータが自動的にストリーミングされます。

パフォーマンス最適化のポイント

  1. コンポーネントの分割粒度'use client'ディレクティブの配置は慎重に行う。できるだけ末端のコンポーネントに限定し、サーバーコンポーネントの割合を最大化する
  2. データフェッチの並列化:独立したデータ取得はPromise.allで並列実行します
  3. キャッシュ戦略fetchのキャッシュオプションを適切に設定し、不要なサーバー処理を削減する
  4. Partial Prerendering:静的部分と動的部分を組み合わせることで、初期表示のTTFBをさらに短縮できる

RSCはまだ発展途上の技術ですが、フロントエンドアーキテクチャの未来を形作る重要な要素です。内部の仕組みを理解した上で活用することで、ユーザー体験とデベロッパー体験の両方を最大化できるでしょう。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア