投稿日:
更新日:

Pages Router から App Router に移行する

Authors

目次


はじめに

Next.js v13.4 で安定版(stable)がリリースされた App Router は Server Components を活用した新しいルーティングとレンダリング機能を提供しています。

この WEB サイトも Pages Router から App Router に置き換えたので、今回のブログでは App Router の紹介を交えつつ、移行プロセスについてまとめたいと思います。

App Router(App Directory)については こちらのブログ で紹介しています。

App Router で提供される機能

直感的なファイルベースルーティング

Pages Router では各ページと API ルートが混在する構造となっているため、WEB サイトが肥大化してくるとディクトリがカオスにながちです。

App Router ではファイルシステムの構造がそのまま URL 構造に対応するため、Pages より直感的です。

App Router では URL に対応するファイルの発見可能性が改善されており、直感的にコードベースを理解することができます。

Server Components による最適化

従来の Pages Router ではすべてのコンポーネントがクライアントサイドで実行されるため、次のような課題がありました。

  • 初期表示の遅延

サーバから空の HTML が送信され、JavaScript がブラウザでダウンロード・実行されるまでユーザはコンテンツを見ることができません。 その結果、初期表示が遅くなり、特にネットワークが遅い環境やモバイルデバイスでのユーザ体験が悪化します。

  • SEO への悪影響

検索エンジンのクローラやソーシャルメディアの Bot において JavaScript が無効となっている環境でアクセスした場合、実際のコンテンツを取得できないため、SEO への悪影響や OGP 表示の問題が生じます。

  • Core Web Vitals スコアの悪化

全ページのロジックがクライアントに送信されるため、JavaScript バンドルサイズが大きくなり、Core Web Vitals スコア(LCP、FID、CLS)の悪化に繋がります。

App Router の Server Components は、これらの問題を根本的に解決するため、サーバサイドで事前に HTML を生成し、必要最小限の JavaScript のみをクライアントに送信します。

TypeScript 統合とエラーハンドリングの改善

Pages Router ではデータフェッチ関数とコンポーネントの型が分離されがちでした。

App Router では型安全性が大幅に向上します。

Pages Router では各ページでエラー処理を個別に実装する必要がありましたが、App Router では専用のエラー境界(Error Boundary)により、統一されたエラーハンドリングが可能になります。

React 18+ の機能活用

App Router は React 18 で導入された最新機能をフル活用できるよう設計されており、従来の Pages Router では実現困難だった高度なパフォーマンス最適化が可能になりました。

Concurrent Rendering による UI 応答性の向上

Concurrent Rendering は、重い処理が実行されている間でも UI の応答性を維持します。 従来の同期的なレンダリングとは異なり、レンダリング作業を小さな Chunk に分割し、必要に応じて中断・再開できます。

Automatic Batching による最適化

React 18 では、複数のステート更新が自動的にバッチ処理されるため、不要な再レンダリングが削減されます。

Suspense for Data Fetching による段階的な読み込み

Suspense により、データフェッチ中のステート管理が簡素化されます。

Server-Side Streaming による段階的な表示

App Router は Streaming を活用し、ページのコンテンツを段階的に配信することで、初期表示時間を大幅に短縮しています。

これらの React 18 の新機能により、Pages Router では実現できなかったパフォーマンス最適化が可能になります。

App Router 移行手順

既存の Pages Router は主に以下のような構成を取っています。

以下のステップで App Router へ移行します。

  1. App Router ディレクトリ構造作成
  2. Root Layout 作成
  3. Providers コンポーネント分離
  4. ページコンポーネント移行
  5. 動的ルート対応
  6. API Routes 移行
  7. 設定ファイル更新

1. App Router ディレクトリ構造作成

まず、App Router で使用する新しいディレクトリ構造を作成します。 従来の pages ディレクトリとは独立して app ディレクトリを作成し、その中に各ページと API ルートに対応するフォルダを準備します。

この段階では、Pages Router と App Router を並行して運用できるため、段階的な移行の場合は特に有用な方法です。 App Router は pages ディレクトリよりも優先される ため、同じルートが存在する場合は App Router のページが表示されます。

2. Root Layout 作成

App Router の核となる Root Layout を作成します。 これは従来の _app.tsx_document.tsx の機能を統合したもので、アプリケーション全体の HTML 構造とメタデータを定義します。 metadata API を使用することで SEO 対応も簡潔に記述できます。

suppressHydrationWarning={true} は、テーマシステムによるサーバとクライアント間の初期レンダリング差異を解決するために必要になります。

3. Providers コンポーネント分離

Server Components と Client Components の明確な分離のため、ステート管理やテーマシステム等のクライアントサイド機能を専用のコンポーネントに分離します。 "use client" ディレクティブにより、このコンポーネントがクライアントサイドで実行されることを明示します。

この分離により、Root Layout 自体は Server Components として動作し、パフォーマンスの恩恵を受けつつ、必要な箇所のみクライアントサイド機能を提供できます。

4. ページコンポーネント移行

各ページを App Router の page.tsx ファイルとして移行します。 従来の getStaticPropsgetServerSideProps は不要になり、Server Components 内で直接的にデータを取得できます。

この変更により、データフェッチとレンダリングのロジックが一箇所に集約され、型安全性も向上します。

また、Server Components として動作するため、初期表示が高速化されます。

5. 動的ルート対応

動的ルートの実装方法が大幅に変更されています。 generateStaticParams 関数を使用して静的生成対象のパラメータを定義し、params オブジェクトからルートパラメータを取得します。 ファイル名も [...slug] のような形式で、より直感的になりました。

generateStaticParams は従来の getStaticPaths に相当しますが、より簡潔で型安全性の高い実装が可能です。

また、Server Components として動作するためデータフェッチも直接的に行えます。

6. API Routes 移行

API Routes も新しい形式に移行する必要があります。 従来の単一ファイルから、route.ts ファイル内で HTTP メソッド毎に名前付きエクスポート関数を定義する形式に変更されました。 Web 標準の Request / Response API を使用するため標準的な実装が可能です。

この変更により、複数の HTTP メソッドを同一ファイル内で管理でき、TypeScript の型安全性も向上します。

また、Web 標準の API を使用するため、他の環境へのポータビリティも高くなります。

7. 設定ファイル更新

最後に各種設定ファイルを App Router に対応するよう更新します。

Tailwind CSS

app ディレクトリを監視対象に追加します。

ESLint

App Router では Pages Router 用の一部ルールが不適切なエラーを発生させるため、設定を調整します。

  • no-server-import-in-page:Server Components では直接的なサーバサイドインポートが推奨パターン
  • no-head-element:App Router では metadata API を使用するため <head> 要素の直接使用は不要
  • link-passhref:App Router では passHref プロパティが不要
  • no-img-element:Next.js Image コンポーネントの使用が必須でない場合もある
  • no-html-link-for-pages:App Router のルーティング方式では従来のチェックが適用されない

アーキテクチャの比較

  • 移行前:Pages Router アーキテクチャ
  • 移行後:App Router アーキテクチャ

移行前後のアーキテクチャを比較すると、以下のような違いがあります。

Pages RouterApp Router
エントリーポイント_app.tsx + _document.tsxlayout.tsx で統合
レンダリング戦略ページ単位で選択コンポーネント単位で分離
ステート管理全体で混在Client / Server 明確分離
データフェッチgetStaticProps / getServerSidePropsServer Components 直接
パフォーマンスページ単位最適化コンポーネント単位最適化
開発者目線プロジェクトの肥大化で設定が複雑に直感的で分かりやすい

移行による実測効果

Pages Router から App Router への移行による改善点について実測効果を比較してみます。

バンドルサイズの最適化

  • 移行前の実際のビルド結果
  • App Router 移行後の実測値
  • 実測値の比較
ページタイプPages Router (実測)App Router (実測)改善率
About ページ381 B141 B63.0% 削減
Works ページ1.49 kB176 B88.2% 削減
Blog ページ3.82 kB3.55 kB7.1% 削減
ブログ詳細ページ533 B141 B73.5% 削減

Pages Router の 188 ページ構成(静的 67 ページ + SSG 121 ページ)から、移行後は 198 ページ(静的 147 ページ + SSG 51 ページ)へと 10 ページ増加しながらも、全体的な軽量化を実現しています。

個別 JavaScript の総量では Pages Router は 48 ポストそれぞれが 533B の JavaScript を必要としていたためポストの合計は約 25.6kB でした。 移行後の App Router では 49 ポスト(1 ポスト増加)でありながら、各ポストの JavaScript は 141B まで削減され、全ポスト合計で約 6.9kB と、約 73% 改善されています。

  • First Load JS の最適化
ページタイプPages RouterApp Router変化
軽量ページ164 kB106-113 kB31-35% 削減
高機能ページ198 kB282 kB+42% (機能拡張含む)
共通 Chunk145 kB103 kB29% 削減

Server Components により個別ページの JavaScript が大幅に削減され、クライアントサイドでの処理負荷が軽減されました。

また、効率的なコード分割により共通 Chunk が 145kB から 103kB に最適化され、全体的にバンドルサイズが改善されていることが分かります。

パフォーマンスの最適化

  1. Server Components による最適化

Server Components により、従来クライアントサイドで実行していたレンダリング処理をサーバサイドに移行することで、個別ページで必要な JavaScript が格段に少なくなりました。

  1. 効率的なコード分割

必要な機能のみを切り出して、ページ毎に最小限の JavaScript のみを配信する仕組みが構築されています。

ホットリロードの高速化

Client / Server Components の分離により、変更の影響範囲が明確になり、必要な部分のみが再ビルドされるため、開発時の待機時間が大幅に短縮されました。

MDX コンテンツの変更でも、影響範囲が限定されるためホットリロードが迅速に完了します。

Core Web Vitals の改善

App Router 移行による効果を定量的に評価するため、本番環境 で実測しています。

  • 実測結果
指標測定値App Router による効果
Performance Score49/100JavaScript 最適化は成功
First Contentful Paint (FCP)3.6sネットワーク起因の課題
Largest Contentful Paint (LCP)13.1sネットワーク起因の課題
Total Blocking Time (TBT)30msApp Router 効果あり
Cumulative Layout Shift (CLS)0.214レイアウト安定性要改善
Speed Index9.6sネットワーク起因の課題

App Router 移行によって Total Blocking Time は 30ms でした。 これは個別ページの JavaScript が 141B まで削減されたことが直接的な要因で、メインスレッドのブロッキング時間が大幅に短縮されています。

一方で、FCP / LCP の遅延は主にネットワークレイテンシに起因するもので、App Router の JavaScript 最適化とは別の課題だと思われます。

SEO の強化

Server Components を活用することで、検索エンジンやソーシャルメディアクローラに対してコンテンツを取得しやすくしました。 これにより、JavaScript が無効になっている環境でも完全なコンテンツが表示されるため、SEO の観点で大幅な改善が期待できます。

  • Pages Router でのクライアントサイド処理による制約
  • App Router でのサーバサイド処理

遭遇した問題と解決策

Pages Router から App Router への移行作業では、想定していた以上に多くの問題に直面しました。 これらの問題は主に、App Router の厳密なレンダリングモデルと従来のコードとの互換性に起因したものです。

実際に遭遇した問題の中からいくつかピックアップして解決策を紹介します。

HTML 構造の厳密化エラー

App Router では React の Server Components を活用するため、HTML の構造チェックが Pages Router よりも厳密になっています。

特に、Next.js の Link コンポーネントの仕様変更により、従来動作していたコードがエラーを発生させるケースが多発し、Pages Router では見逃されていたエラーが App Router で表面化しました。

  • 問題のあるコード(Pages Router では動作)
  • 修正後のコード(App Router 対応)

この問題は Next.js 13 で Link コンポーネントの API が変更されたことに起因します。 Pages Router では passHref プロパティが必要でしたが、App Router では不要です。

また、MDX コンテンツ内でのネストした段落要素も問題となりました。

  • 問題のあるコード(MDX でネストエラー)
  • 修正後のコード(ネスト回避)

MDX は Markdown から HTML に変換される際、段落を自動的に <p> タグで囲います。 既に <p> タグで囲まれたコンテナ内に配置すると HTML の仕様違反となるため注意が必要です。

Client / Server Components 分離の複雑さ

App Router の最大の特徴である Client / Server Components の分離は、ステート管理の実装で問題になる場合があり、アプリケーション全体に影響する Context Provider の配置には注意が必要です。

特に問題になったのが、SSR で生成された静的な HTML とクライアントサイドの JavaScript がインタラクティブな動作を付加するプロセス(ハイドレーション)の際に、両者の内容が不一致を起こして発生するエラーです。 このような問題は、一般に Hydration Error として知られています。

テーマカラーのステート管理の例:

  • 問題のあるコード(Server Components エラー)
  • 修正後のコード(Client Components 分離)

この問題の根本原因は、Server Components がサーバサイドで実行され、クライアントサイドの状態(localStorage、useState 等)にアクセスできないことです。 ThemeProvider は内部で useStateuseEffect を使用するため、Client Components として動作する必要があります。

今回は Context Provider を専用の Client Components に切り出し、suppressHydrationWarning を使用してサーバとクライアント間の初期レンダリング差異を解決することで対応しました。 これにより、SSR の恩恵を受けつつ、クライアントサイドの動的機能も正常に動作させることができます。

ESLint 設定の互換性問題

Pages Router 用に設定された ESLint ルールが App Router 環境では不適切なエラーを発生させる問題も頻繁に遭遇しました。 特に、Server Components 内でのインポートやヘッド要素の使用に関するルールが誤検知を引き起こしていました。

  • App Router 対応の ESLint 設定

App Router の Server Components ではサーバサイドモジュールの直接インポートが推奨パターンとなっているため @next/next/no-server-import-in-page は無効化しました。


まとめ

App Router への移行により、単に開発がしやすくなっただけでなくパフォーマンスの点でも大きく改善されました。 App Router の最大の特徴は、ページ数が増加するほど Server Components の恩恵が大きくなることです。 198 ページ構成の現在でも全体的な軽量化を実現しており、スケールメリットが確認できました。

App Router への移行に際して Server Components の理解や諸々の Config 周りの修正対応は大変ですが、移行が未だの場合は検討してみると良さそうです!

参考・引用