🤝

React Server Components (RSC) PayloadをNext.js App Routerと共に理解を深めてみる

2024/12/06に公開

はじめに

本記事では、Next.jsの2つのルーティングシステム、Page RouterとApp Routerの主な違いを解説し、App Routerで導入されたReact Server Components(RSC)、その中核となるRSC Payloadについて学習を深めていきます。

Next.jsでApp Routerが利用できるようになってから、従来のPage Routerとの違いやRSCの仕組みに興味を持っていました。しかし、これらの違いや内部で動作する「RSC Payload」の仕組みについては、言語化できるほど理解が及んでいませんでした。

本記事を通じて、次の点について理解を深めていきます。

  • Page RouterとApp Routerの具体的な違い
  • App Routerがもたらす新しいレンダリングパラダイム
  • RSC Payloadの構造と利点

App RouterとPage Routerの違い

レンダリングモデル

Page Routerの課題

Page Routerでは、getStaticPropsgetServerSidePropsを使用してサーバーサイドレンダリングを実現できますが、その場合でもページ全体のハイドレーションが必要です。

すべてのコンポーネントに対してハイドレーションが実行されるため、初期表示後に一時的な処理負荷が発生し、ブラウザのメインスレッドがブロックされます。その結果、ユーザーの操作に遅延が生じる可能性があり、とくに低スペックデバイスで顕著な問題となります。

※ここで言うハイドレーションとは、サーバーから送られたHTMLに対して、Reactがクライアント側でイベントリスナーの付与やstate管理の初期化を行うプロセスを指します

App Routerの利点

App Routerは、デフォルトですべてのコンポーネントがserver componentとして動作し、RSC Payloadを使用して効率的なデータ転送を実現します。さらに、client componentのみがハイドレーションの対象になるため、初期表示後の処理負荷を大幅に削減できます。

ストリーミング

Page Routerの課題

Page Routerでは、ページ全体のロードが完了するまでユーザーは何も表示を見ることができません。とくに大規模なページや複数のデータフェッチが必要なページで、ユーザー体験を損なう可能性があります。

App Routerの利点

App Routerでは、コンポーネント単位でのストリーミングが可能になりました。

各コンポーネントが準備できた順に表示され、重いデータフェッチを含むコンポーネントが他の表示をブロックしません。また、loading.js<Suspense>を使用して細かい制御が可能で、初期コンテンツをすぐに表示しながら残りのコンテンツを読み込むことができます。

さらに、サーバーサイドでのレンダリング処理を並列化し、各コンポーネントの処理完了を待たずに転送を開始することで、ネットワーク帯域を効率的に利用できます。

バンドルサイズ

Page Routerの課題

Page Routerでは、クライアントサイドのJavaScriptバンドルサイズが大きくなりがちです。

これは、すべてのコンポーネントがJavaScriptとして配信され、UIライブラリやコンポーネントのコード全体が含まれるためです。また、ハイドレーション処理やデータ取得コードもバンドルに含まれることで、サイズが増大します。

App Routerの利点

App Routerは、Server Componentsを活用することで、バンドルサイズを大幅に最適化できます。

これにより、初期ロード時間の短縮やネットワーク帯域の使用量削減が実現し、とくに低速回線や低スペックデバイスで大きな効果を発揮します。また、JavaScriptの解析時間も短縮されることで、First Interactiveまでの時間が改善されます。

ただし、クライアントサイドのJavaScriptバンドルサイズと、RSC Payloadの転送量はトレードオフの関係にあります。繰り返しレンダリングされるコンポーネントは、RSC Payloadの転送量を抑えるためにClient Componentにするほうが望ましい場合もあります。

https://zenn.dev/akfm/books/nextjs-basic-principle/viewer/part_2_client_components_usecase#rsc-payload転送量の削減

RSC Payloadとは?

RSC Payloadは、サーバーサイドでレンダリングされたReactコンポーネントの結果をクライアントに効率的に送信するための特殊なデータ構造です。

従来のサーバーサイドレンダリング(SSR)とは異なり、必要なデータのみを最適化して送信する、よりスマートなアプローチを提供します。

RSC Payloadの構造

RSC Payloadは主に以下の要素で構成されています。

  1. server componentのレンダリング結果
  2. client componentのplaceholder
  3. コンポーネントツリーの構造情報

上記の情報がJSONライクな形式で効率的にパッケージングされ、クライアントに送信されます。

RSC Payloadの利点

  1. 効率的なデータ転送:必要最小限のデータのみをクライアントに送信し、ネットワーク転送量を削減します。
  2. 高速な初期表示:server componentの結果が直接DOMに挿入されるため、クライアントサイドでの追加のレンダリングが不要になります。
  3. インタラクティブ性の即時提供:クライアントコンポーネントのみがハイドレーションされるため、ページの特定の部分だけを素早くインタラクティブにできます。
  4. ストリーミング対応:RSC Payloadはチャンク単位でストリーミング可能です。これにより、大規模なアプリケーションでも、ユーザーは部分的なコンテンツをより早く見られます。

ここで言う「チャンク」とは、RSC Payloadを分割して送信する際の各部分を指します。これにより、大きなページでも重要な部分から順次表示することが可能になります。

RSC Payloadの実装例

Next.jsのApp Routerを使用すると、RSC Payloadの処理が自動的に最適化されます。以下は簡単な実装例です。

app/page.tsx
import { ClientComponent } from '@/components/client-component'
import { ServerComponent } from '@/components/server-component'

export default async function Page() {
  const data = await fetchData() // サーバーサイドでデータフェッチ

  return (
    <div>
      <ServerComponent title={data.title} />
      <ClientComponent initialData={data.content} />
    </div>
  )
}
components/server-component.tsx
export const ServerComponent = ({ title }) => {
  return <h1>{title}</h1>
}
components/client-component.tsx
'use client'

import { useState } from 'react'

export const ClientComponent = ({ initialData }) => {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{initialData}</p>
      <button onClick={() => setCount(count + 1)}>
        Clicked {count} times
      </button>
    </div>
  )
}

この例では、Pageコンポーネントがサーバーサイドでレンダリングされ、そのレンダリング結果とともにClientComponentのplaceholderがRSC Payloadとしてクライアントに送信されます。

これにより、初期表示が迅速に行われ、クライアント側でのハイドレーションが必要な部分のみが後から処理されるため、全体的なパフォーマンスが向上します。

また、ClientComponentはインタラクティブな要素を持ち、ユーザーの操作に応じて動的に更新されるため、ユーザー体験が向上します。

Compositionパターンの活用

RSC Payloadを効果的に活用するには、Compositionパターンを使用することが推奨されます。これにより、server componentとclient componentを適切に組み合わせ、パフォーマンスとインタラクティブ性のバランスを取ることができます。

以下は、Compositionパターンを用いた実装例です。

app/page.tsx
import { ClientComponent } from '@/components/client-component'
import { ServerComponent } from '@/components/server-component'

export default async function Page() {
  const data = await fetchData() // サーバーサイドでデータフェッチ

  return (
    <ClientComponent>
      <ServerComponent title={data.title} />
    </ClientComponent>
  )
}
components/server-component.tsx
export const ServerComponent = ({ title }) => {
  return <h1>{title}</h1>
}
components/client-component.tsx
'use client'

export const ClientComponent = ({ children }) => {
  return <div>{children}</div>
}

この例では、ClientComponentchildrenとしてServerComponentを受け取ります。これにより、以下の利点があります。

  1. 効率的なレンダリングServerComponentはサーバーサイドでレンダリングされ、その結果がRSC Payloadの一部としてクライアントに送信されます。これにより、初期ロード時間が短縮され、クライアントでの再レンダリングが最小限に抑えられます。
  2. 柔軟な構造とインタラクティブ性の向上ClientComponentは子要素としてServerComponentを受け取り、必要に応じてステート管理やイベントハンドリングを行うことができます。これにより、初期表示の速度とインタラクティブ性を両立させています。
  3. コード分割の容易さ:サーバーサイドとクライアントサイドのロジックを明確に分離でき、大規模なアプリケーションの保守性が高まります。

このCompositionパターンを使用することで、RSC Payloadの利点を最大限に活かしつつ、柔軟で効率的なコンポーネント構造を実現できます。これはとくに大規模なアプリケーションや、パフォーマンスが重要な場面で有効です。

まとめ

本記事では、Next.jsのPage RouterとApp Routerの主な違いを解説し、とくにApp Routerで導入されたRSC Payloadの仕組みについて詳しく見てきました。

App Routerは、以下の革新的な特徴を持っています。

  • Server Componentsをデフォルトとした効率的なレンダリングモデル
  • コンポーネント単位でのストリーミングによる段階的なUI表示
  • RSC Payloadによる最適化されたデータ転送

これらの特徴により、従来のPage Routerでの課題が解決され、より高速で効率的なWebアプリケーションの開発が可能になりました。App RouterとRSC Payloadの理解を深め、プロジェクトの要件に応じて適切に活用することで、より良いユーザー体験を提供できるでしょう。

GitHubで編集を提案
LCL Engineers

Discussion