言語を切り替える
テーマを切り替える

Next.js Pages Router から App Router への移行実践ガイド:段階的戦略と回避すべき落とし穴

先週の金曜日の午後3時、技術責任者が会議室でこう切り出しました。「この Next.js 12 のプロジェクト、14 にアップグレードできるかな?」

私は画面上の、2年間稼働している古いプロジェクトを見つめ、少し緊張しました。正直な第一印象は「絶対にやめておこう」でした。アップグレードしたくないわけではありませんが、前回の React 17 へのアップグレードで、バグ修正にまるまる1週間費やし、カスタマーサポートの電話が鳴り止まなかったからです。

しかし、今回は少し違うようです。

家に帰って公式ドキュメントを読み始め、App Router の新機能――Server Components、ネストされたレイアウト、パフォーマンス向上など――を見て、心が動きました。しかし、移行ガイドのページを開いた途端、頭を抱えました。API の対照表が画面いっぱいに広がり、getServerSideProps はどう変えるのか、_app.js はどう分割するのか……悩みの種は尽きません。

さらに厄介なのは、公式が推奨する「段階的な移行」は聞こえはいいものの、実際に試してみると、/pages/app の間でページを移動する際にユーザーがローディングスピナーを見せられ、体験がかえって悪化することでした。

私はまるまる2週間を費やして、落とし穴にはまり、コミュニティの議論を読み漁り、さまざまな解決策を試しました。そして最終的に、比較的信頼できる移行戦略をまとめました。この記事では、以下の実践的な経験を共有します:

  • プロジェクトが移行に値するかどうかを判断する方法
  • 2つの移行戦略の実際の長所と短所(公式ドキュメントの理論ではありません)
  • getServerSideProps 移行の詳細な手順とコード例
  • 私が個人的に遭遇した7つの大きな落とし穴とその解決策

もしあなたがアップグレードすべきかどうか迷っているか、すでに移行を始めているが問題に直面しているなら、この記事が少しでも回り道を避ける助けになれば幸いです。

なぜ移行するのか? まずは損益計算から

移行について話す前に、水を差すようですが、すべてのプロジェクトが苦労して移行する価値があるわけではありません。

先月、ある友人から、まもなく終了するキャンペーンページを App Router にアップグレードすべきかと聞かれました。私は即座に止めました。半年後に削除されるコードに、なぜ時間を無駄にする必要があるのでしょうか?

では、どのようなプロジェクトが移行に値するのでしょうか? 私はいくつかの判断基準をまとめました:

ネストされたレイアウトの必要性

これが私たちのチームが移行した主な理由です。私たちの SaaS 管理画面には、サイドバー + トップナビゲーション + コンテンツエリアという3層構造のレイアウトがあります。Pages Router を使用していたときは、ページを切り替えるたびにサイドバー全体が再レンダリングされていました。

ユーザーが新しいページを開くと、画面全体が一瞬ちらつくのがはっきりと分かります。ネットワークが遅いのではなく、レイアウトの再描画が原因です。

App Router のネストされたレイアウトはこの問題を完璧に解決しました。移行後、WorkOS チームは「ログイン体験が劇的に改善し、ロード状態やレイアウトの揺れがなくなった」と報告しています。私たち自身のテストでも同様で、ユーザーがページを切り替えても、コンテンツエリアのみが更新され、ナビゲーションバーは不動のままです。

パフォーマンス最適化の余地

プロジェクトの初回読み込み時間(First Contentful Paint)が3秒を超えているなら、App Router が役立つかもしれません。

私たちには商品一覧ページがあり、以前は getServerSideProps でデータを取得していたため、リフレッシュのたびにサーバーが HTML 全体をレンダリングするのを待つ必要がありました。Server Components に変更した後、リストデータをサーバー側で取得し、ストリーミングでクライアントに直接送信できるようになり、初回表示時間は3.2秒から1.8秒に短縮されました。

ただし、落とし穴があります。すべてのページが速くなるわけではありません。純粋なクライアントインタラクションのページ(例えばキャンバスエディタ)は、移行後も基本的には変わらず、抽象化のレイヤーが増えることで逆に遅くなる可能性さえあります。

長期メンテナンスプロジェクト

プロジェクトを3年以上メンテナンスする予定なら、早めに移行した方が得です。Vercel はすでに、将来の新機能は App Router を優先してサポートし、Pages Router は「メンテナンスモード」に入ると明言しています。

2年後にまた移行したくはありません。その頃には API が変わっているかもしれず、踏むことになる地雷は増える一方でしょう。

推奨されない移行シナリオ

しかし、以下の場合は急いで移行しないことをお勧めします:

  • まもなく終了するプロジェクト —— 必要ありません
  • 小規模な静的サイト(5ページ以内) —— 利益が小さすぎて割に合いません
  • チームが React 18 に詳しくない —— まず Suspense と Server Components を理解しましょう
  • 大量の古いサードパーティライブラリに依存している —— 多くのライブラリが互換性がないことに気付くでしょう

いろいろ言いましたが、核心は一言です:移行のために移行しないでください。まず自問してください。移行によってどのような実際の問題が解決されるのか?「特にないけど、新しいものを試したい」なら、やめておいた方がいいでしょう。

私たちのチームは損益計算をしました:2週間の人的リソースを投入し、ユーザー体験の向上と今後3年間の技術的負債の削減を得る。これは価値があります。あなたのプロジェクトはどうでしょうか?

2つの移行戦略の選択

公式ドキュメントでは「段階的移行」が推奨されています。聞こえは安全そうです――ゆっくりと、1ページずつ移行するのです。

しかし、試してみたところ、これには致命的な問題があることが分かりました。

段階的移行の落とし穴

このシナリオを想像してください:トップページを /app ディレクトリに移行したが、商品詳細ページはまだ /pages にあるとします。ユーザーがトップページから商品ページに移動すると、ページが突然白くなり、ローディングが回り始め……終わってようやくコンテンツが表示されます。

なぜでしょうか? App Router から Pages Router に移動すると、Next.js はそれらを2つの独立したアプリケーションとして扱い、JavaScript バンドル全体を再読み込みする必要があるからです。ユーザー体験は瞬時に2010年に逆戻りします。

WorkOS チームもブログでこれについて不満を漏らしています。「異なるルーター間でのナビゲーションは、無関係な2つのアプリ間をジャンプするようなものだ」。彼らも最初は段階的移行を考えましたが、後に諦めました。

では、段階的移行は完全に不可能なのでしょうか? そうではありません。

段階的移行が適しているシナリオ

  • ページ間の結合度が非常に低い(ブログなど、記事間に関連性が少ない)
  • モジュール単位で完全に移行できる(例えば、ユーザーセンターモジュール全体を先に移行し、次に商品モジュールを移行するなど)
  • 切り替え時のローディング状態を許容できる

ある技術ブログがこれを行っているのを見ましたが、効果は悪くありませんでした。しかし、SaaS 製品や EC サイトでは考えられません。

WorkOS のゼロダウンタイム戦略

では、複雑なプロジェクトはどうすればいいのでしょうか? WorkOS は巧妙な解決策を提示しました。

彼らのやり方はこうです:/app の下に一時ディレクトリ /app/new を作成し、すべてのページをそこで書き直し、クエリパラメータでどのバージョンにアクセスするかを制御します。

少し複雑に聞こえるかもしれませんが、コードを見れば明らかです:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/:path*',
        destination: '/new/:path*',
        has: [
          {
            type: 'query',
            key: 'new',
            value: 'true',
          },
        ],
      },
    ]
  },
}

これにより、一般ユーザーが /dashboard にアクセスすると古いバージョンが使用されますが、?new=true を付けると新しいバージョンが表示されます。

テスト担当者、プロダクトマネージャー、デザイナーは本番環境で事前に新バージョンを検証でき、ユーザーは全く気付きません。新バージョンのテストが完了し問題がないことを確認したら、/app/new 全体を /app に変更し、/pages を削除して完了です。

私たちのチームはこの方法を採用しました。移行プロセス全体を通して、正式リリースの前に実際のデータを使って1週間テストしていたため、ユーザーはバグに遭遇しませんでした。

具体的な手順

  1. Next.js を 14 にアップグレード —— /pages は触らず、フレームワークのバージョンだけ上げる
  2. ルーティングフックの移行 —— next/routernext/navigation に変更し、コードが両方のルーターと互換性を持つようにする
  3. /app/new ディレクトリの作成 —— ここでページ構造を再構築する
  4. 既存コンポーネントの再利用 —— /pages 内の React コンポーネントを直接インポートする。書き直す必要はない
  5. rewrites の設定 —— 上記の設定を追加し、?new=true で切り替える
  6. 内部テスト + カナリアリリース —— チームに新バージョンを使ってもらい、問題が見つかり次第修正する
  7. 正式リリース —— /app/new/app に移動し、rewrites と /pages を削除する

私たちは手順1から7まで、合計10営業日かかりました。そのうち6日間はページの書き直し、3日間はバグ修正、最後の1日はリリース作業でした。

私の提案

あなたのプロジェクトが:

  • 10ページ未満で、ページ間が独立している → 段階的移行
  • 10ページ以上で、ユーザー体験の要件が高い → ゼロダウンタイム戦略
  • 新規プロジェクト → 最初から App Router、迷う必要なし

新機能の開発と並行して移行しようと思わないでください。私は試しましたが、両側のコードスタイルが全く異なり、見ていて辛くなりました。2週間集中して終わらせるか、しばらく動かさないかのどちらかにしましょう。

getServerSideProps 移行の実践

これは私が最もよく聞かれる質問です。「getServerSideProps が使えなくなったけど、データはどうやって取得するの?」

実際には、App Router のデータ取得はよりシンプルですが、考え方を変える必要があります。

「分離」から「統合」へ

Pages Router のロジックはこうでした:データ取得(getServerSideProps)と UI(コンポーネント)を別々に書き、Next.js がサーバー側でデータ関数を呼び出し、結果をコンポーネントに渡す。

App Router ではこの方式をやめました。ページコンポーネント自体が非同期関数になり、その中で直接データを取得します:

// ❌ 古い書き方: pages/project/[id].tsx
export async function getServerSideProps(context) {
  const { id } = context.params
  const res = await fetch(`https://api.example.com/projects/${id}`)
  const project = await res.json()

  return {
    props: { project }
  }
}

export default function ProjectPage({ project }) {
  return <h1>{project.title}</h1>
}
// ✅ 新しい書き方: app/project/[id]/page.tsx
export default async function ProjectPage({ params }) {
  const { id } = params
  const res = await fetch(`https://api.example.com/projects/${id}`, {
    cache: 'no-store' // 重要!getServerSideProps の挙動と同じ
  })
  const project = await res.json()

  return <h1>{project.title}</h1>
}

シンプルに見えますよね? しかし、ここには2つの大きな落とし穴があります。

落とし穴1:cache 設定の間違い

デフォルトでは、App Router の fetch は キャッシュされますgetStaticProps と同じ)。リクエストのたびに新しいデータを取得するわけではありません。

移行当初、私はこれに気付かず、商品価格ページを移行したところ、価格が更新されないという問題が発生しました。ユーザーから「値下げされたはずなのに、元の価格のままだ」とクレームが入り、半日調査してようやくキャッシュのせいだと分かりました。

この対応表を覚えておいてください

  • getServerSidePropscache: 'no-store'
  • getStaticPropscache: 'force-cache'(デフォルトの挙動)
  • getStaticProps + revalidatenext: { revalidate: 60 }

落とし穴2:クライアント状態はどうする?

元々 getServerSideProps を使用していたページでも、フィルタリングや並べ替えなどのクライアントインタラクションがあることがよくあります。

App Router に移行すると、非同期 Server Component では useStateuseEffect などのフックが使えないことに気付きます。

どうすればいいでしょうか? コンポーネントを分割します。

// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
  const products = await fetchProducts() // サーバーでデータを取得

  return <ProductList initialData={products} /> // Client Component に渡す
}
// components/ProductList.tsx (Client Component)
'use client' // この行に注意!

import { useState } from 'react'

export function ProductList({ initialData }) {
  const [products, setProducts] = useState(initialData)
  const [filter, setFilter] = useState('')

  // クライアントのフィルタリングロジック
  const filtered = products.filter(p => p.name.includes(filter))

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  )
}

このように、サーバーはデータの取得を担当し、クライアントはインタラクションを担当するという役割分担が明確になります。

ただし注意点として、“use client” を乱用しないでください。ページ全体を “use client” とマークしている人を見たことがありますが、それでは Server Components の意味がなくなってしまいます。

実際の移行手順

私はこの2段階のプロセスを採用しました:

ステップ1:コンポーネントの分割
まず元の pages ディレクトリ内で、コンポーネントを「純粋な表示用」と「状態あり」の2つに分割し、テストをパスさせます。

ステップ2:app ディレクトリへの移動

  • 純粋な表示部分を app/[route]/page.tsx に置き、非同期関数としてマークし、その中でデータを取得します。
  • 状態のある部分を独立したファイルに抽出し、“use client” を追加します。
  • getServerSideProps のコードを削除します。

この方法の利点は、万が一問題が発生してもすぐにロールバックでき、両側のコードが混乱しないことです。

もう一つの小さな詳細

以前 context.req.cookies を使ってユーザー ID を読み取っていた箇所は、次のように変更します:

import { cookies } from 'next/headers'

export default async function Page() {
  const cookieStore = cookies()
  const token = cookieStore.get('auth-token')

  // token を使ってユーザーデータをリクエスト...
}

同様に headers()redirect() なども next/headersnext/navigation からインポートします。公式ドキュメントに完全なリストがありますので、ここでは省略します。

7つの一般的な落とし穴と解決策

さて、ここからが本番です。以下の7つの落とし穴は私がすべて踏んだもので、それぞれ解決するのに少なくとも1時間はデバッグしました。

落とし穴1:サーバーエラーが飲み込まれる

現象:ページがレンダリングされず、エラーも出ない。ローディングのスケルトン画面や空白が表示されるだけ。

私の経験:API 呼び出しを変更したところ、ページが真っ白になりました。コンソールを開いてもエラーはありません。データが返ってきていないと思い、console.log を大量に追加しましたが、問題は見つかりませんでした。

最終的に、サーバーが例外をスローしていたものの、私が error.tsx を設定していなかったため、Next.js がエラーを飲み込み、Suspense の fallback を表示していたことが分かりました。

解決策:各ルートディレクトリに error.tsx を追加します。

// app/dashboard/error.tsx
'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>エラーが発生しました:{error.message}</h2>
      <button onClick={reset}>再試行</button>
    </div>
  )
}

これを追加すれば、少なくともエラー情報は確認できます。開発環境では Next.js が詳細なスタックトレースを表示し、本番環境ではユーザーフレンドリーなエラーメッセージを表示します。

落とし穴2:useRouter が機能しない

現象useRouter().push() がジャンプしない、またはメソッドが存在しないというエラーが出る。

原因next/routernext/navigation は2つの異なる API であり、互換性がありません。

私は最初、インポートパスを変えるだけでいいと思っていました:

// ❌ 間違い
import { useRouter } from 'next/navigation'

const router = useRouter()
router.push('/dashboard') // push メソッドが存在しない!

後で知りましたが、next/navigationuseRouter には push メソッドがなく、別の関数を使う必要があります:

// ✅ 正しい方法
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

const router = useRouter()
router.push('/dashboard') // これは存在するが、挙動が異なる

// または Link コンポーネントを直接使用する
import Link from 'next/link'
<Link href="/dashboard">ジャンプ</Link>

対照表(私は PC の横に貼り、移行中ずっと見ていました):

Pages RouterApp Router
useRouter().push(url)useRouter().push(url)(あるが推奨されない)
useRouter().pathnameusePathname()
useRouter().queryuseSearchParams()
useRouter().asPathusePathname() + useSearchParams()

落とし穴3:動的インポートの失敗

現象next/dynamic でインポートしたコンポーネントがレンダリングされず、コンソールに “You’re importing a component that needs useState. It only works in a Client Component…” というエラーが出る。

原因:Server Component はデフォルトでサーバー側でレンダリングされますが、一部の client-only ライブラリ(チャートライブラリなど)はエラーになります。

以前、ECharts を使ったチャートページがありました:

// ❌ これはエラーになる
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), { ssr: false })

export default function Page() {
  return <Chart data={data} />
}

エラーによると、Chart コンポーネントは window オブジェクトを必要としますが、サーバーにはありません。

解決策page.tsx"use client" を追加するか、Chart を独立した Client Component に抽出します:

// app/charts/page.tsx
import { ClientChart } from './ClientChart'

export default function Page() {
  return <ClientChart />
}
// app/charts/ClientChart.tsx
'use client'

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), { ssr: false })

export function ClientChart() {
  return <Chart data={data} />
}

落とし穴4:ページ切り替え時のちらつき

現象:リンクをクリックしてジャンプすると、ページ全体が再レンダリングされ、トップナビゲーションバーやサイドバーが一瞬ちらつく。

原因layout.tsx の設定が間違っているか、レイアウトを全く使用していない。

App Router の核心的な利点はネストされたレイアウトですが、私は最初それを十分に活用せず、ナビゲーションバーを各 page.tsx に直接書いていました。当然ちらつきます。

正しい方法

// app/layout.tsx (ルートレイアウト、全ページ共有)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header /> {/* トップナビゲーション、再レンダリングされない */}
        {children}
      </body>
    </html>
  )
}
// app/dashboard/layout.tsx (ダッシュボードレイアウト)
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar /> {/* サイドバー、ダッシュボード内のページ切り替え時に再レンダリングされない */}
      <main>{children}</main>
    </div>
  )
}

こうすれば、ユーザーが /dashboard/analytics/dashboard/settings の間で行き来しても、main 領域のみが更新され、サイドバーは微動だにしません。

落とし穴5:404 ページが効かない

現象:カスタム 404 ページが表示されず、Next.js のデフォルトのページが表示される。

原因:Pages Router の 404.js と App Router の not-found.tsx が競合している。

移行中、/pages/404.js が残ったままだったため、App Router の /app/not-found.tsx が機能しませんでした。

解決策/pages/404.js/pages/500.js を削除し、App Router の規約を使用します:

// app/not-found.tsx
export default function NotFound() {
  return <h1>ページが存在しません</h1>
}

page.tsx で手動で 404 をトリガーする場合:

import { notFound } from 'next/navigation'

export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound() // 404 をトリガー
  }

  return <div>{data.title}</div>
}

落とし穴6:開発サーバーがどんどん遅くなる

現象:起動直後は問題ないが、何度かコードを変更するとホットリロードが10秒待ちになり、最終的にはクラッシュする。

正直な話:これも完全には解決できていません。

これは Next.js 14 の既知の問題であり、FlightControl チームもブログで「dev server のパフォーマンスが悪すぎて、それを避けるためならすべての新機能を諦めてもいい」と不満を述べていました。彼らは20分ごとに開発サーバーを再起動しているそうです。

私たちのチームの体験も似たようなものでした。

一時的な解決策

  • 定期的に dev server を再起動する(私は15分のリマインダーを設定しました)
  • next dev --turbo を使用して実験的な Turbopack を有効にする(速くなりますが、たまにバグがあります)
  • 不必要な Server Component を減らす。Client Component で十分なページもあります

Next.js 15 でこの問題が改善されると言われていますが、まだ試していません。

落とし穴7:サードパーティライブラリの非互換性

現象:特定のアニメーションライブラリ(Framer Motion, Lottie)が、window や document が見つからないというエラーを出す。

原因:これらのライブラリは純粋なクライアントサイドのものであり、Server Component では使用できません。

以前 Framer Motion を使ってページ遷移アニメーションを作っていましたが、移行後に全滅しました。

解決策

  1. これらのライブラリを使用するコンポーネントを "use client" でラップする
  2. ライブラリが React 18 をサポートする更新バージョンを出しているか確認する(すでに対応しているものもあります)
  3. どうしてもダメなら、SSR をサポートするライブラリに乗り換える

特に以下の一般的な client-only ライブラリに注意してください:

  • Framer Motion(App Router でのページ終了アニメーションに問題があり、公式 issue で議論されています)
  • swiper, slick-carousel などのカルーセルライブラリ
  • 各種チャートライブラリ(ECharts, Chart.js など)
  • ドラッグ&ドロップライブラリ(react-dnd, dnd-kit など)

プロジェクトがこれらのライブラリに強く依存している場合は、移行前に GitHub の issue を確認し、互換性を確かめることをお勧めします。

移行後の最適化提案

移行完了はゴールではありません。まだ最適化の余地があります。

クライアント JavaScript の削減

これは Server Components の最大の利点です。

私たちの元の製品一覧ページは、React コンポーネントだけで 120KB(gzip 圧縮後)ありました。移行後、データ表示部分を Server Component に変更し、フィルタリングと並べ替えのみを Client Component にしたところ、バンドルサイズは 45KB に減少しました。

確認方法

npm run build

出力を見て、どのページが (Static) または (SSR) で、どれが (Client Component を使用していることを示す)かを確認します。画面中が だらけなら、"use client" を使いすぎている可能性があります。

最適化のコツ

  • 静的コンテンツ(テキスト、画像)は Server Component に置く
  • インタラクションコンポーネント(フォーム、ボタン)は Client Component にする
  • ページ全体を "use client" にせず、必要な子コンポーネントだけにする

キャッシュの適切な使用

App Router のキャッシュ戦略は Pages Router よりもはるかに複雑です。

// キャッシュしない、リクエストごとに新しいデータを取得(リアルタイムデータに最適)
fetch(url, { cache: 'no-store' })

// 60秒間キャッシュし、その後再検証(頻繁に更新されるがリアルタイム性は不要なデータ)
fetch(url, { next: { revalidate: 60 } })

// 永久キャッシュ(不変の静的データに最適)
fetch(url, { cache: 'force-cache' })

私たちの商品一覧では60秒の revalidate を採用し、データが古くなりすぎないようにしつつ、サーバー負荷を軽減しました。リリース後、API 呼び出し量は60%減少しました。

パフォーマンス監視

移行前後で以下の指標を比較してください:

  • First Contentful Paint (FCP) - ユーザーが最初のコンテンツを見るまでの時間
  • Time to Interactive (TTI) - ページが完全に対話可能になるまでの時間
  • Cumulative Layout Shift (CLS) - ページ読み込み時にレイアウトが揺れるかどうか

Vercel Analytics で監視したところ、移行後に FCP は3.2秒から1.8秒に、TTI は5.1秒から3.3秒に短縮されました。

ただし、すべてのページが速くなるわけではありません。純粋なクライアントインタラクションのページ(キャンバスエディタなど)は、移行後もほとんど変化しませんでした。

過剰最適化に注意

Server Component を使いたいがために、無理にコンポーネントを分割しないでください。

私は以前、あるフォームを20個の小さなコンポーネントに分割し、「細かく分けるほど Server Component の比率が上がる」と考えていました。結果、コードの可読性が下がり、同僚は理解できず、メンテナンスコストがかえって増加しました。

経験則:コンポーネントが useState / useEffect を必要とするなら、迷わず "use client" を付けてください。Server Components はツールであり、KPI ではありません。

結論

長くなりましたが、核心となるポイントをまとめます:

移行は流行を追うためではなく、実際の問題を解決するためのものです。プロジェクトがネストされたレイアウトを必要としている、クライアント JS を削減したい、あるいは長期的なメンテナンスを考えているなら、App Router に時間を投資する価値があります。

適切な移行戦略を選ぶ:小規模プロジェクトは段階的移行でゆっくりと、大規模プロジェクトはゼロダウンタイム戦略で一気に切り替えます。開発と移行を同時に行おうとしないでください。混乱します。

落とし穴にはまるのは正常です。私が挙げた7つの落とし穴は氷山の一角に過ぎません。問題に遭遇したら、まず GitHub で issue を検索してください。90%の落とし穴は他の誰かがすでに踏んでいます。

最適化しすぎないでください。Server Components はツールであり、目的ではありません。コードの可読性とチームの効率は、バンドルサイズよりも重要です。

移行に関しての私のアドバイスは、まず1〜2ページを選んで試験的に行い、プロセスを実行して経験をまとめ、それから全面的に推進することです。私たちのチームもそうしました。最初のページは3日かかりましたが、コツをつかんだ後の10ページは5日で終わりました。

最後に、Next.js App Router には確かに多くの問題(特に dev server のパフォーマンス)がありますが、全体的な方向性は間違っていません。エコシステムが成熟すれば、これらの落とし穴は徐々に埋まっていくでしょう。

移行プロセスで問題に遭遇したら、ぜひコメントで議論してください。私も同じ落とし穴にはまったことがあるかもしれません。


関連リソース:

移行がうまくいくことを祈っています!

Next.js Pages Router から App Router への完全移行フロー

評価からリリースまでの完全な移行ステップ。2つの戦略の選択とよくある問題の解決策を含みます。

⏱️ Estimated time: 80 hr

  1. 1

    Step1: プロジェクトが移行に値するか評価する

    判断基準:
    • ネストされたレイアウトの必要性:多層レイアウトが必要で、ページ切り替え時に再レンダリングしたくない
    • パフォーマンス最適化の余地:初回読み込み時間が3秒を超えており、最適化の余地がある
    • 長期メンテナンス:プロジェクトを3年以上メンテナンスする予定があり、早めに移行してメリットを享受したい

    推奨されない移行シナリオ:
    • まもなく終了するプロジェクト
    • 小規模な静的サイト(5ページ以内)
    • チームが React 18 に詳しくない
    • 大量の古いサードパーティライブラリに依存している
  2. 2

    Step2: 移行戦略を選択する

    プロジェクトの規模に応じて選択:

    小規模プロジェクト(<10ページ、ページが独立)→ 段階的移行:
    • 1ページずつ移行する
    • 切り替え時のローディング状態を許容できる
    • ブログなどページ間の結合度が低いプロジェクトに適している

    大規模プロジェクト(>10ページ、高いユーザー体験が必要)→ ゼロダウンタイム戦略:
    • /app/new ディレクトリですべてのページを再構築する
    • rewrites + クエリパラメータでバージョン切り替えを制御する
    • 内部テストに合格してから正式にリリースする
  3. 3

    Step3: getServerSideProps の移行

    手順:
    1. ページコンポーネントを非同期関数に変更する
    2. コンポーネント内で直接データを fetch する
    3. 正しい cache オプションを設定する:
    • getServerSideProps → cache: 'no-store'
    • getStaticProps → cache: 'force-cache'
    • getStaticProps + revalidate → next: { revalidate: 60 }

    4. クライアントインタラクション部分を Client Component に分割する:
    • サーバーでデータを取得し、Client Component に渡す
    • Client Component で useState, useEffect などのインタラクションロジックを処理する
  4. 4

    Step4: ルーティングとナビゲーションの処理

    ルーティング関連のコードを更新する:
    • next/router → next/navigation
    • useRouter().pathname → usePathname()
    • useRouter().query → useSearchParams()
    • router.push() の代わりに Link コンポーネントを使用する

    注意:next/navigation の useRouter の挙動は Pages Router と異なるため、Link コンポーネントを直接使用することを推奨します
  5. 5

    Step5: レイアウトシステムの設定

    ネストされたレイアウトを利用してページ切り替え時のちらつきを防ぐ:
    • app/layout.tsx をルートレイアウト(Header, Footer)として作成する
    • 機能エリア用のサブレイアウト(例:app/dashboard/layout.tsx)を作成する
    • 各層の layout はその層に特有の UI 要素のみを追加する
    • サブレイアウトは親レイアウトを自動的に継承し、切り替え時に再レンダリングされない
  6. 6

    Step6: エラーと 404 の処理

    エラー処理:
    • error.tsx を作成してエラーをキャッチし、フレンドリーなヒントを表示する
    • error コンポーネントには 'use client' をマークする

    404 処理:
    • /pages/404.js を削除する
    • app/not-found.tsx を作成する
    • page.tsx 内で notFound() 関数を使用して 404 をトリガーする
  7. 7

    Step7: テストと最適化

    テストのポイント:
    • すべてのページルートが正常かテストする
    • データ取得が正しいか検証する
    • クライアントインタラクションが正常か確認する
    • レイアウト切り替えがちらつかないか確認する

    パフォーマンス最適化:
    • 不必要な "use client" マークを減らす
    • キャッシュ戦略を適切に使用する
    • FCP, TTI, CLS などの指標を監視する
    • 移行前後のパフォーマンスデータを比較する

FAQ

段階的移行とゼロダウンタイム移行の違いは何ですか?
段階的移行は1ページずつ移行する方法で、小規模プロジェクトに適していますが、/pages と /app 間で切り替える際にローディング状態が発生します。

ゼロダウンタイム移行は /app/new ディレクトリですべてのページを再構築し、クエリパラメータでバージョン切り替えを制御し、テスト合格後に正式リリースする方法で、大規模プロジェクトかつ高いユーザー体験が求められるシナリオに適しています。
getServerSideProps 移行後のデータ取得はどうなりますか?
ページコンポーネントを非同期関数に変更し、コンポーネント内で直接データを fetch します。

cache オプションの設定に注意してください:
• getServerSideProps は cache: 'no-store' に対応
• getStaticProps は cache: 'force-cache' に対応

ページにクライアントインタラクションがある場合は、Server Component(データ取得)と Client Component(インタラクション処理)に分割する必要があります。
移行後、ページ切り替え時にちらつくのはなぜですか?
通常、layout システムが正しく使用されていないためです。app/layout.tsx をルートレイアウトとして作成し、機能エリア用のサブレイアウトを作成すべきです。そうすれば、ページ切り替え時にコンテンツエリアのみが更新され、ナビゲーションバーやサイドバーは再レンダリングされず、ちらつきを防げます。
App Router で useRouter はどのように使用しますか?
App Router は next/router ではなく next/navigation を使用します。useRouter().pathname は usePathname() に、useRouter().query は useSearchParams() に変更されます。router.push() ではなく、Link コンポーネントを直接使用してナビゲーションすることをお勧めします。
移行後、サードパーティライブラリが互換性がない場合はどうすればいいですか?
それらのライブラリを使用するコンポーネントに 'use client' をマークしてください。

よくある非互換ライブラリ:
• Framer Motion
• チャートライブラリ(ECharts, Chart.js)
• カルーセルライブラリなど

移行前にライブラリの GitHub issue を確認し、互換性を確かめることをお勧めします。
開発サーバーが遅くなる問題を解決するには?
これは Next.js 14 の既知の問題です。

一時的な解決策:
• 定期的に dev server を再起動する(15分推奨)
• next dev --turbo を使用して Turbopack を有効にする
• 不必要な Server Component を減らす

Next.js 15 で改善されると言われています。
移行にはどれくらいの時間がかかりますか?
プロジェクトの規模によります:
• 小規模プロジェクト(<10ページ)は3〜5日
• 大規模プロジェクトは2〜3週間

まずは1〜2ページを選んで試験的に行い、プロセスを実行してから全面的に推進することをお勧めします。私たちのチームは最初のページに3日かかりましたが、後の10ページは5日で終わりました。

9 min read · 公開日: 2025年12月18日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事