React Server Components(RSC)は、Reactの歴史において最も大きなパラダイムシフトの一つです。コンポーネントをサーバー側で実行し、そのレンダリング結果をストリーミングでクライアントに送信するこのアーキテクチャは、従来のCSR(Client-Side Rendering)やSSR(Server-Side Rendering)とは根本的に異なるアプローチを取っています。
本記事では、RSCの内部的な仕組み、特にストリーミングプロトコルの動作原理を深掘りし、実践的な活用パターンを解説します。Next.js App Routerの裏側で何が起きているのかを理解することで、より効果的なアプリケーション設計が可能になります。
RSCのストリーミングは、独自のワイヤーフォーマットを使用してサーバーからクライアントにデータを送信します。このフォーマットは、コンポーネントツリーをシリアライズ可能な形式に変換したものです。
サーバーコンポーネントがレンダリングされると、その結果はReact Flightプロトコルと呼ばれる形式でエンコードされます。これは、JSONに似た独自のストリーミング形式で、以下の特徴を持ちます。
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>
)
}
この例では、データベースアクセスはサーバーコンポーネントで行い、インタラクティブなフィルタリング機能はクライアントコンポーネントに委譲しています。
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は、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を呼ぶことで、関連するサーバーコンポーネントが再実行され、最新のデータが自動的にストリーミングされます。
'use client'ディレクティブの配置は慎重に行う。できるだけ末端のコンポーネントに限定し、サーバーコンポーネントの割合を最大化するPromise.allで並列実行しますfetchのキャッシュオプションを適切に設定し、不要なサーバー処理を削減するRSCはまだ発展途上の技術ですが、フロントエンドアーキテクチャの未来を形作る重要な要素です。内部の仕組みを理解した上で活用することで、ユーザー体験とデベロッパー体験の両方を最大化できるでしょう。