Next.js App Router / React Server Components(RSC)を紐解いてみた


デザインエンジニアの表(@HirokiOmote)です。

Next.jsでApp Routerがリリースされて、1年が過ぎました。
弊社では、@hiroppyさんを技術顧問に迎え、Frontendを中心とした長期的な技術選定にご協力いただきました。
本日は、そこで得た学びをご紹介したいと思います。

App Routerについて

2023年5月にNext.js 13.4がstableとしてリリースされ、App Routerが登場しました。
ツリー構造でのファイル配置が基本となりました。

ディレクトリ構成とルーティング

page単位・feature(機能)ごとに切り分けたディレクトリ構成が可能になったため、より直感的で再利用性の高い構造が実現しました。

// App Router
.
├── dashboard
│   ├── components
│   │   ├── button.tsx
│   │   └── graph.tsx
│   └── page.tsx
└── page.tsx

// Pages Router
.
├── components
│   └── dashboard
│       └── components
│           ├── button.tsx
│           └── graph.tsx
└── pages
    ├── dashboard
    │   └── index.tsx
    └── index.tsx

比較表

柔軟なデザインパターンやルーティングが可能になり、サーバー内の処理を増やすことで、ブラウザ上での負担を減らすような設計になっています。

機能App RouterPages Router
ルーティングサーバー中心クライアント中心
ファイル参照の優先度優先App Routerでマッチしない場合
コンポーネントのデフォルトServer ComponentsClient Components
パフォーマンス
ルーティングの柔軟性高い低い
レイアウトコンポーネントネスト・動的な変更が可能静的

では、Pages Routerの未来はないのか?

Pages Routerがすぐに使えなくなる?

安心してください。そんな心配はありませんでした。
Next.jsのメンテナーによると、Pages Routerはこれから数年間、サポートされるとのことでした。(もしかしたらもっと長期かも)

新機能はApp Routerで

Server ActionParallel Routesをはじめ、新機能はApp Routerを中心に実装されていくそうです。
反面、ファイルやディレクトリ名のルール、パフォーマンス向上のためのキャッシュ戦略など学習コストが増えたことは事実です。
一方で、Pages Routerではルーティングのルールをはじめ、比較的シンプルな使い勝手です。

  • Pages Router…小規模かつシンプルなアプリケーション
  • App Router…大規模かつ複雑なアプリケーション

という棲み分けになりそうです。

App RouterのデフォルトはReact Server Components

App Routerでは、React Server Components (RSC)というアーキテクチャが採用されました。
Next.js独自の機能ではなく、React18でCanaryリリースされた機能をNext.jsが先行してビルトインしています。

Server Components (SC)はこれまでSSRやCSRとは異なる仕組みで動作し、サーバーのみでレンダリングされるコンポーネントです。
App RouterではコンポーネントはデフォルトでSCとして扱われます。

SCが登場する前は、全てのコンポーネントはClient Components (CC)として扱われていました。
命名から勘違いしそうですが、従来のNext.jsのコンポーネントがServerとClientで分割されたわけではありません。
(私は勘違いしました)

つまり、Pages Routerで扱われていたコンポーネントのメンタルモデルはそのままに、SCのレンダリングプロセスが新たに追加されることになります。

従来のメンタルモデル 
引用:Why do Client Components get SSR'd to HTML? · reactwg server-components · Discussion #4 · GitHub

SCのレンダリングプロセスが追加
引用: Why do Client Components get SSR'd to HTML? · reactwg server-components · Discussion #4 · GitHub

従来のPages Routerにおけるレンダリングプロセス

Pages Routerでのコンポーネントのレンダリングプロセスを説明します。
Server Components (SC)に対し、従来のコンポーネントはClient Components (CC)と呼ばれます。

  1. ユーザーのリクエストから、サーバー上でシリアライズ化されたJSONを元に生のHTMLをレンダリング
  2. サーバーがブラウザにJSONと同じコンポーネントを組み立てるデータを返す
  3. ブラウザでインタラクティブな操作が可能なJavaScript(React)を追加(Hydration)
  4. ブラウザで操作可能に

これが、Server Side Renderingです。
1で復元可能なHTMLとJSONを生成し、3でJavaScriptを追加することで、インタラクティブな操作ができるようなコンポーネントに復元します。

Hydrationのメリット

  • サーバーでHTMLがレンダリングされるので、転送量を減らせる
  • ブラウザ側でユーザーが操作可能になる時間を短縮
  • HTMLはリクエスト時にサーバーで生成されるので、SEOの向上やアクセシビリティが強化される

CCのレンダリングプロセス

ラーメンの調理で例えると…

Server Side Rendering (SSR)

  • サーバーサイドでラーメンの具材を揃えて、スープも作り、それを食卓(ブラウザ)に送ります
  • クライアントは調理の手間を省いた状態で、ラーメンの具材を受け取ります
  • クライアント側でお湯を注ぐHydrationという作業が必要です

Hydration

  • サーバーから送られてくるのは乾燥した麺とスープの素です
  • 食卓(ブラウザ)でお湯を注いで、ラーメン(コンポーネント)を完成させます

サーバーから送られてきた静的なHTML(乾麺)とクライアント側で実行されるJavaScript(お湯)が結合して、完全に機能するアプリケーション(ラーメン)を作り上げます。

CCは「インスタント麺を受け取り、利用側でお湯を注いで完成するコンポーネント」と覚えてください。

じゃあ、Server Componentsって何するの?

App Routerでコンポーネントを扱う際は基本的にServer Components (SC)として扱われると説明しました。

SCとCCの違いですが、SCはHydrationをせず、サーバー上でレンダリングしたHTMLだけをブラウザに送信するだけに留めます。

また、キャッシュを用いたパフォーマンス最適化ができます。
キャッシュを用いてのレンダリングはIncremental Static Regeneration (ISR)でも行われていましたが、App Routerではコンポーネント単位でキャッシュをより細かく制御することが可能になりました。

また、シークレットキーをSCで読み出して、dataのfetchをした上でレンダリングを行うことができます。シークレットキーはロジック部分と同じくビルド時に秘匿され、静的に生成された状態で出力されます。

SCはAPIやDBの接続をサーバー側で行い、結果のみをレンダリングするコンポーネントです。
反面、stateuseEffectを用いた状態管理は行うことはできません。

ここもラーメンの調理に例えてみましょう。

Server Components (SC)

  • SCは、完成されたラーメンをサーバー側で作り上げ、食卓(ブラウザ)にそのまま送ります。
  • クライアントはすぐに食べられるので、追加の調理(Hydration)は不要です。
  • 秘伝のスープの製造方法(シークレットキー)などは、クライアントには教えません。

SCという新たな宅配の仕組みにより「完成されたラーメンをお届けする」ことが可能になったと覚えてください。

SCとCCの使い分けと機能の比較

window documentなどブラウザのみのAPIに対するアクセスやcustom hooksを使う際はCCとして扱う必要があります。
また、状態をブラウザで管理するなどJavaScriptを実行する場合などは、明示的にuse clientと宣言したら従来通りの使い方が可能です。

以下はSCとCCの使い分けの比較です。

Server ComponentsClient Components
Data Fetch×
Backendのリソースに(直接)アクセス×
機密情報をサーバーに保管(アクセストークン、API キーなど)×
npm modulesをサーバーのみに使用 / クライアントサイドの JavaScript を減らす×
インタラクティブ・イベントリスナー(onClick()、onChange()など)を追加する×
状態とライフサイクル(useState(), useReducer(), useEffect()など)×
ブラウザ専用の API を使用する×
状態、エフェクト、またはブラウザ専用 API に依存するカスタムフックを使用する×
React クラスcomponentsを使用×

使い分けがわからない場合、乱暴な考えですが、全てのComponentに対し、use clientと宣言しておけば、これまで通りの開発プロセスは維持できるでしょう。

しかし、おすすめはできません。ご説明いたします。

構造

SCはappディレクトリに近い、ツリーの上段で扱われることを想定されています。
ディレクトリ構造は、予め、指針を決めた上で開発に臨むべきだと考えます。

CCでimportされたコンポーネントは、全てCCとして扱われます。
逆に、CCでSCをimportすることはできません。
これを可能にすると、Hydrationの際に、SCに記載された秘匿情報がブラウザで復元=重要な情報の漏洩を意味しているからです。

RSC、ひいてはApp Routerの扱いを難しくしている原因のひとつです。

図のように、SCは上位に、CCは末端に配置されることになるでしょう。

SCとCCのツリー構造
引用:https://www.plasmic.app/blog/how-react-server-components-work

とはいえ、CCでSCの制御を行いたいシーンはあります。
タブやアコーディオン内でdataのfetchをしつつ、ユーザーの操作によって表示・非表示を行いたい場合です。
これはSCをchildrenとして扱うことで解決できます。

// Client Components(CC)
export default function ClientComponent({ children }) {
  return (
    <div>
      <h1>Hello from client land</h1>
      {children}
    </div>
  )
}

// Server Component(SC)
export default function ServerComponent() {
  return <span>Hello from server land</span>
}

// OuterServerComponent.server.jsx
// OuterServerComponent can instantiate both client and server
// components, and we are passing in a <ServerComponent/> as
// the children prop to the ClientComponent.
import ClientComponent from './ClientComponent.client'
import ServerComponent from './ServerComponent.server'
export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

これでブラウザ上での状態管理を行いつつも、SCの表示を制御できます。

  • SC…Data Fetchを行う
  • CC…表示・非表示などのブラウザの状態を管理する

以上のように、SCとCCの責務を把握した上で、ディレクトリの構成をするべきでしょう。
後からSC/CCへの分割を試みても、密結合になったコンポーネントを分割するのは至難の業です。

これが、全てCCとして扱うべきではない理由です。

App RouterとPages Routerの共存

全てCCとして扱いたいシーンの一つに、Pages Routerで構成したページをApp Routerに移行したい時だと思います。

実はApp RouterとPages Routerは共存ができます。
すなわち、Pages Routerを使った既存プロダクトもページや機能ごとに、段階的に移行することが可能です。

.
├── app // 新規ページ
│   └── new-page
│       ├── components
│       │   ├── input.tsx
│       │   └── section.tsx
│       └── page.tsx
├── components // 既存ページのコンポーネント
│   └── dashboard
│       ├── button.tsx
│       └── graph.tsx
└── pages // 既存ページ
    ├── dashboard.tsx
    └── index.tsx

注意点ですが、Pages Routerで生成されたページからApp Routerで生成されたページに遷移する際、0.01%の確率で404エラーが出るバグがあります。
※2024/08現在確認されているバグ

解決策や原因など、詳しくはこちらのブログをご覧ください。
App Router移行時に0.01%の確率でCSR遷移が404エラーになる - とろろこんぶろぐ

Next.jsの未来、PPRについて

SSR/SSG/ISRもサーバーでページの生成を行いますが、その範囲はページ全体に及びます。
反面、部分的なレンダリングには対応できません。

これは、Partial Pre Rendering(PPR)において、可能になります。
next.config.js Options: Partial Prerendering (experimental) | Next.js

2024/07現在、PPRはCanary Releaseの機能です。

サーバーで事前に静的なレンダリングをしつつ、部分的な動的レンダリングの双方を組み合わせた新しいレンダリングモデルです。

ECサイトを例に挙げましょう。
商品画像や説明は事前にレンダリング(SSG)し、カート情報・おすすめ商品といった部分は、ユーザーの情報に応じて、サーバー側で動的にレンダリング(SSR)を行えるようになります。

引用: Learn Next.js: Partial Prerendering | Next.js

Pages Routerでパフォーマンス向上を目的とする場合、ページ単位での事前レンダリングできませんでした。
これを改善し、機能や特定のUIごとに動的に部分レンダリングできるようになったのがPPRという機能です。

SSG+部分的なSSR=PPRと覚えておくといいでしょう。

まとめ

App Routerはルーティングが変わり、ツリー構造によるディレクトリ設計が可能になりました。
RSCがベースになるので、よりコンポーネントの責務を意識する必要があります。
仕組みの根本を理解した上で、SC/CCの使い分けを行うと、適切なデザインパターンが見つかるのではないでしょうか。

App Routerの理解の早道は慣れること!
弊社でも一部のプロダクトではApp Routerで開発をはじめました。

また、既存のプロダクトでも新規ページを実装するときなど、小さくはじめてから段階的な移行が可能です。
徐々に取り組んで行ってみてはいかがでしょうか。

用語集

略称
RSCReact Server Components
SCServer Components
CCClient Components
SSRServer Side Rendering
SSGStatic Site Generation
ISRIncremental Static Regeneration

参考文献

最後に

esiteではスケールの大きいプロダクト開発が徐々に増えてきています。
パフォーマンス向上のために学んだ技術なども、活かしていける環境です。

Frontendに関心がある方、ぜひ一度お話ししましょう!

hrmos.co

© 2019- estie, inc.