🧑‍⚖️

#85 レンダリング方式で見るPage RouterとApp Routerの違い

に公開

はじめに

以前、Next.jsを使用しているアプリケーションで「Page RouterからApp Routerに移行するべきか」という議題が上がることがありました。
その時の結論としては「現状、移行の必要はない」ということで落ち着きましたが、この辺りの前提知識が不足していると感じたため、これらの具体的な違いについて調べてみました。
今回の記事では、その内容を備忘録としてまとめていきたいと思います。

Page RouterからApp Routerへ

それまでNext.jsはPage Routerのみでしたが、v13から安定版(stable)としてApp Routerがリリースされました。


詳細は後述しますが、Page RouterとApp Routerではルーティングのディレクトリ構成が変わります。
ちなみに、App Routerで使用するディレクトリはPage Routerで使用するディレクトリと連携して動作することができるため、仮にPage RouterからApp Routerへ移行、となった場合でもページ単位の段階的な導入が可能です。
ただし、App RouterとPage Routerが共存している場合のルートはApp Routerが優先されるそうなので、移行時には気をつけたいポイントです。
以下、公式ドキュメントからの引用文です。

Good to know: The App Router takes priority over the Pages Router. Routes across directories should not resolve to the same URL path and will cause a build-time error to prevent a conflict.

引用:https://nextjs.org/docs/app/building-your-application/routing#the-app-router

Page RouterとApp Routerの違い

ルーティング

Next.jsはファイルシステムベースのルーティングを採用しています。
ざっくりと言えば、フォルダによってルートを定義している、といったところでしょうか。


先にも述べているように、Page RouterとApp Routerではルーティングの方法が変わりました。ルートを定義するディレクトリの構成が変わったとも言えます。


■Page Router
src/pagesフォルダ配下に置いたフォルダ・ファイルをルーティングします。

src
  └ pages
      ├ index.js     -> /
      ├ about.js     -> /about
      └ users
          ├ index.js -> /users
          ├ list.js  -> /users/list
          └ [id].js  -> /users/1, /users/2,,,(動的ルート)


■App Router
src/appフォルダ配下に置いたフォルダ・ファイルをルーティングします。
ルーティングに使用したいファイルは、ファイル規約に従ってファイル名をpageとします。

src
  └ app
      ├ page.js          -> /
      ├ about
          └ page.js      -> /about
      └ users
          ├ page.js      -> /users
          ├ list
              └ page.js  -> /users/list
          └ [id]
              └ page.js  -> /users/1, /users/2,,,(動的ルート)

レンダリング

App RouterではデフォルトのコンポーネントがRSC(React Server Component)に変わりました。
(※RSCはReact Canaryの機能です。が、詳細は後述します)
これにより、以下のような点からパフォーマンスを向上させることができるようになりました。

  • 配信バンドルサイズの軽量化
    • サーバーコンポーネントはその処理結果のみをブラウザに送信するため、より容量の少ないファイルを送ることができる(※処理結果の方が容量が大きくなる場合はこの限りではないので、留意が必要です→参考
  • バックエンドに直接アクセスできる
    • レンダリングに必要なデータの取得にかかる時間とクライアント側で行うリクエストの数を削減できる

(その他のサーバーレンダリングの利点についてはこちら


また、React18でストリーミングによるサーバー側のレンダリングがサポートされたことで、コンポーネント単位でのサーバーサイドレンダリング(SSR)が可能となりました。

React 18でSSRされたページは<Suspense/>で区切られた単位ごとに非同期で生成されて読み込まれます。

例えばページの一部分がサーバ側でデータの取得を行なってからレンダリングする「重い」コンポーネントだとします。そのコンポーネントを<Suspense/>で囲んでおくと、その部分のレンダリングを後回しにしつつページの他の部分をブラウザ上で表示することができます。(中略)

React 17以前ではサーバサイドレンダリングをこのように分割して行うことができなかったため、一部のコンポーネントのレンダリングに時間がかかる場合ページ全体の表示までの時間に影響していました。

引用:React 18の新機能と並行レンダリング革命

こうした機能による非同期SSRの実現も、RSCでのパフォーマンス向上を後押ししています。

RSCとは

ではそもそもRSCとは何か、これを説明するためにまずは従来のReactのレンダリング方法から確認していきましょう。


押さえておきたいポイントは、
Reactで実装したアプリケーションは「すべてクライアントサイドレンダリング(CSR)で行うシングルページアプリケーション(SPA)である」ということです。


ひとつずつ確認していきましょう。
CSRを簡単にまとめると、以下のように説明されます。

クライアントからのリクエストに対して、サーバーは空のHTMLとJavaScriptを返します。

このJavaScriptがブラウザ上で実行されることにより、実際に表示するHTMlをレンダリングします。

引用:【Next.js14】CSR・SSR・SSG・ISRの違いと実装方法


SPAの課題として「初回ローディング時にHTMLやJavaScriptなどすべてのデータを取得する必要があるため、表示までに時間がかかる」というものがあります。
Reactでは仮想DOMを用いてレンダリング時のDOMの差分のみを更新することができるため、初回以降のレンダリングは早いです。しかし、依然として初期表示には時間がかかってしまうという問題は残されていました。


以下に示しているようにSPAの課題解消に向けた技術も登場しているため、初期表示までの時間を短縮することも可能ではあります。

このローディング時間短縮のため、サーバー側で描画するHTMLをあらかじめ生成するSSR(Server Side Rendering)やSSG(Static Site Generator)といった技術が登場しています。

これらの手法を組み合わせることでSPAのデメリットを減らすことも可能となりました。

引用:https://devlog.mescius.jp/spa-javascript-framework-in-2020/#i-3


そして、こうしたSSRやSSGといった技術をレンダリング方式に採用しているフレームワークとして、Page Routerを用いたNext.jsが挙げられます。
詳細については公式ドキュメントこちらを参考いただくのがわかりやすいと思います。
簡単にまとめてしまうと、Next.jsのレンダリング方式はそれぞれ

  • Page Route
    • SSR(Server Side Rendering)
    • SSG(Static Site Generation)
    • ISR(Incremental Static Regeneration)
    • CSR(Client Side Rendering)
  • App Router
    • RSC(React Server Component)
    • SSG(Static Site Generation)
      fetch()で使用可能
    • ISR(Incremental Static Regeneration)
      fetch()+revalidateで使用可能

となっています。


ここで本題の「RSCとは」について確認していきましょう。

まず、「RSCとはなにか」という部分ですが、一言でいうとコンポーネント単位でレンダリング方式をSSRまたはCSRに分けることができる技術です。

引用:【Next.js】RSCとクライアントコンポーネントを改めて理解する


上記にあるコンポーネントはそれぞれ

  • SSRでレンダリングされるコンポーネント = サーバーコンポーネント
  • CSRでレンダリングされるコンポーネント = クライアントコンポーネント

と呼びます。


そしてApp Routerでは、デフォルトコンポーネントを「サーバーコンポーネント」としています。
クライアントコンポーネントとして使用したい場合は、ファイルの1行目に"use client"と定義します。


※use clientについて
クライアントコンポーネントとして宣言した時、その適用範囲は子コンポーネントにも及びます。
親コンポーネントと子コンポーネントでそれぞれ"use client"を定義するとエラーが返されてしまいますので、クライアントコンポーネントの宣言は親コンポーネントだけで行いましょう。

サーバーコンポーネントとクライアントコンポーネントの使い分け

どのような時にどちらのコンポーネントを使用するべきかについて、Next.jsの公式ドキュメントに使用例がまとめられています。

  • サーバーコンポーネントを使用する
    • データの取得処理やビジネスロジック部分
    • バックエンドリソースに直接アクセスする処理
    • アクセストークンやAPIキーなどの機密情報をサーバーに保持する処理
    • サーバーへの大きな依存関係を維持し、クライアント側へのJavaScriptを削減したいとき
  • クライアントコンポーネントを使用する
    • onClick()やonChange()などのイベントリスナー・インタラクティブ機能
    • useState()やuseEffect()などのUIやユーザー操作に関連する要素
    • ブラウザのみで利用するAPI
    • useState()やuseEffect()、ブラウザのみで利用するAPIなどのカスタムフック
    • Reactのクラスコンポーネント

ここからもわかるように、App Routerのデフォルトはサーバーコンポーネントですが、クライアントコンポーネントを使わずに実装することはまず無理です。
セキュリティやパフォーマンス向上の観点を意識しつつ、アプリの性質や要件に応じて、適切にコンポーネントを配置することが必要になりそうです。

おわりに

今回はNext.jsのPage RouterとApp Routerの違いについて、ルーティングとレンダリング方式の観点でまとめてみました。


調べていく中で、App Routerにするメリットとして「パフォーマンスの向上」が多く取り上げられている印象を受けました。やはりSPAの課題である「初期表示までの時間の長さ」をより短縮できる、というのが強みなのかなと感じています。


本記事では、RSCについては文量も多くなってしまうためそこまで深く取り上げることはしませんでしたが、React Hooks ライブラリであるSWRとの比較も個人的に気になっているので、機会があれば取り上げてみようと思います。
いずれにせよ、今回のPage RouterとApp Routerの比較を通して、レンダリング関連の知識を整理することができたので良かったです。


今回はここまで。
最後まで閲覧いただき、ありがとうございます。




参考:

Discussion