Vite から Next.js への移行方法

このガイドでは、既存の Vite アプリケーションを Next.js に移行する手順を説明します。

移行する理由

Vite から Next.js に移行する主な理由は以下の通りです:

初期ページ読み込みの遅さ

Vite のデフォルト React プラグインで構築されたアプリケーションは、純粋なクライアントサイドアプリケーション(SPA)です。SPA では初期ページの読み込みが遅くなる傾向があります。これには以下の理由があります:

  1. ブラウザは、データ読み込みリクエストを送信する前に、React コードとアプリケーションバンドル全体のダウンロードと実行を待つ必要がある
  2. 新機能や依存関係を追加するたびにアプリケーションコードが肥大化する

自動コード分割の欠如

コード分割を手動で行おうとすると、かえってパフォーマンスが悪化する可能性があります。Next.js にはルーターに組み込まれた自動コード分割機能があります。

ネットワークウォーターフォール

クライアント-サーバー間でデータ取得リクエストが連鎖的に発生すると、パフォーマンスが低下します。Next.js ではサーバーサイドでのデータ取得が可能で、この問題を解消できます。

高速で意図的なローディング状態

React Suspense を使ったストリーミングの組み込みサポートにより、UI のどの部分をどの順序で読み込むかをより意図的に制御できます。これにより、読み込みが高速でレイアウトシフトのないページを構築できます。

データ取得戦略の選択

Next.js では、ページやコンポーネントごとにデータ取得戦略を選択できます。ビルド時、サーバーでのリクエスト時、クライアントサイドでの取得など、ニーズに応じて最適な方法を選べます。

ミドルウェア

Next.js ミドルウェアを使用すると、リクエスト完了前にサーバー上でコードを実行できます。認証が必要なページに未認証ユーザーがアクセスした際のフラッシュを防いだり、国際化や実験的な機能に役立ちます。

組み込み最適化

画像フォントサードパーティスクリプトはアプリケーションのパフォーマンスに大きな影響を与えます。Next.js にはこれらを自動的に最適化するコンポーネントが組み込まれています。

移行手順

この移行の目的は、できるだけ早く動作する Next.js アプリケーションを取得し、その後段階的に Next.js の機能を採用できるようにすることです。最初は既存のルーターを移行せず、純粋なクライアントサイドアプリケーション(SPA)として維持します。これにより、移行中の問題発生リスクやマージコンフリクトを最小限に抑えられます。

ステップ 1: Next.js 依存関係のインストール

まず next を依存関係としてインストールします:

Terminal
npm install next@latest

ステップ 2: Next.js 設定ファイルの作成

プロジェクトルートに next.config.mjs を作成します。このファイルにはNext.js の設定オプションが含まれます。

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // シングルページアプリケーション(SPA)として出力
  distDir: './dist', // ビルド出力ディレクトリを `./dist/` に変更
}

export default nextConfig

補足: Next.js 設定ファイルには .js または .mjs が使用できます。

ステップ 3: TypeScript 設定の更新

TypeScript を使用している場合、Next.js と互換性を持たせるために tsconfig.json を以下のように更新します。TypeScript を使用していない場合はこのステップをスキップできます。

  1. tsconfig.node.json へのプロジェクト参照を削除
  2. include 配列./dist/types/**/*.ts./next-env.d.ts を追加
  3. exclude 配列./node_modules を追加
  4. compilerOptionsplugins 配列{ "name": "next" } を追加: "plugins": [{ "name": "next" }]
  5. esModuleInteroptrue に設定: "esModuleInterop": true
  6. jsxpreserve に設定: "jsx": "preserve"
  7. allowJstrue に設定: "allowJs": true
  8. forceConsistentCasingInFileNamestrue に設定: "forceConsistentCasingInFileNames": true
  9. incrementaltrue に設定: "incremental": true

変更を加えた tsconfig.json の例:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

TypeScript 設定の詳細は Next.js ドキュメントを参照してください。

ステップ 4: ルートレイアウトの作成

Next.js のApp Router アプリケーションには、アプリケーション内のすべてのページをラップするルートレイアウトファイル(React Server Component)が必要です。このファイルは app ディレクトリの最上位に定義されます。

Vite アプリケーションでルートレイアウトに相当するのはindex.html ファイルで、<html>, <head>, <body> タグが含まれています。

このステップでは index.html ファイルをルートレイアウトファイルに変換します:

  1. src フォルダ内に新しい app ディレクトリを作成
  2. app ディレクトリ内に新しい layout.tsx ファイルを作成:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}
export default function RootLayout({ children }) {
  return '...'
}

補足: レイアウトファイルには .js, .jsx, .tsx 拡張子が使用可能です。

  1. index.html ファイルの内容を <RootLayout> コンポーネントにコピーし、body.div#rootbody.script タグを <div id="root">{children}</div> に置き換えます:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js にはデフォルトで meta charsetmeta viewport タグが含まれているため、<head> からこれらを安全に削除できます:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. favicon.ico, icon.png, robots.txt などのメタデータファイルは、app ディレクトリの最上位に配置されていれば自動的にアプリケーションの <head> タグに追加されます。サポートされているファイルapp ディレクトリに移動した後、<link> タグを安全に削除できます:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 最後に、Next.js はMetadata APIを使用して残りの <head> タグを管理できます。最終的なメタデータ情報をmetadata オブジェクトに移動します:
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

以上の変更により、index.html にすべてを宣言する方法から、Next.js のフレームワークに組み込まれた規約ベースのアプローチ(Metadata API)に移行しました。このアプローチにより、SEO とページの共有性をより簡単に向上させることができます。

ステップ5: エントリポイントページの作成

Next.jsでは、page.tsxファイルを作成することでアプリケーションのエントリポイントを宣言します。Viteにおけるmain.tsxファイルに相当するものです。このステップでは、アプリケーションのエントリポイントを設定します。

  1. appディレクトリ内に[[...slug]]ディレクトリを作成

このガイドではまずNext.jsをSPA(シングルページアプリケーション)として設定することを目指しているため、アプリケーションのすべてのルートをキャッチするページエントリポイントが必要です。そのため、appディレクトリ内に新しい[[...slug]]ディレクトリを作成してください。

このディレクトリはオプショナルキャッチオールルートセグメントと呼ばれるものです。Next.jsはファイルシステムベースのルーターを使用しており、フォルダがルートを定義します。この特別なディレクトリにより、アプリケーションのすべてのルートが含まれるpage.tsxファイルに誘導されます。

  1. app/[[...slug]]ディレクトリ内に新しいpage.tsxファイルを作成し、以下の内容を記述:
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // 後で更新します
}
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // 後で更新します
}

豆知識: ページファイルには.js.jsx.tsx拡張子が使用できます。

このファイルはサーバーコンポーネントです。next buildを実行すると、このファイルは静的アセットにプリレンダリングされます。動的コードは一切必要ありません。

このファイルはグローバルCSSをインポートし、generateStaticParamsに対して/のインデックスルートのみを生成するように指示しています。

次に、クライアントのみで実行されるViteアプリケーションの残りを移動させましょう。

'use client'

import React from 'react'
import dynamic from 'next/dynamic'

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

export function ClientOnly() {
  return <App />
}
'use client'

import React from 'react'
import dynamic from 'next/dynamic'

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

export function ClientOnly() {
  return <App />
}

このファイルは'use client'ディレクティブで定義されたクライアントコンポーネントです。クライアントコンポーネントは、クライアントに送信される前にサーバー上でHTMLにプリレンダリングされます。

最初はクライアントのみのアプリケーションにしたいので、Appコンポーネント以下のプリレンダリングを無効にするようにNext.jsを設定できます。

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

次に、エントリポイントページを更新して新しいコンポーネントを使用します:

import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}
import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

ステップ6: 静的画像インポートの更新

Next.jsは静的画像のインポートをViteとは少し異なる方法で扱います。Viteでは、画像ファイルをインポートするとその公開URLが文字列として返されます:

App.tsx
import image from './img.png' // 本番環境では`image`は'/assets/img.2d8efhg.png'になります

export default function App() {
  return <img src={image} />
}

Next.jsでは、静的画像のインポートはオブジェクトを返します。このオブジェクトはNext.jsの<Image>コンポーネントと直接使用するか、既存の<img>タグでオブジェクトのsrcプロパティを使用できます。

<Image>コンポーネントには自動画像最適化という追加の利点があります。<Image>コンポーネントは、画像の寸法に基づいて結果の<img>widthheight属性を自動的に設定します。これにより、画像が読み込まれる際のレイアウトシフトを防ぎます。ただし、片方の寸法のみがスタイリングされ、もう片方がautoにスタイリングされていない画像がアプリに含まれている場合、問題が発生する可能性があります。autoにスタイリングされていない場合、その寸法は<img>の寸法属性の値にデフォルト設定され、画像が歪んで表示される可能性があります。

<img>タグを保持することで、アプリケーションの変更量を減らし、上記の問題を防ぐことができます。その後、必要に応じてローダーを設定して<Image>コンポーネントに移行するか、自動画像最適化を備えたデフォルトのNext.jsサーバーに移行することで、画像を最適化できます。

  1. /publicからインポートされた画像の絶対インポートパスを相対インポートに変換:
// 変更前
import logo from '/logo.png'

// 変更後
import logo from '../public/logo.png'
  1. 画像オブジェクト全体ではなくsrcプロパティを<img>タグに渡す:
// 変更前
<img src={logo} />

// 変更後
<img src={logo.src} />

あるいは、ファイル名に基づいて画像アセットの公開URLを参照することもできます。例えば、public/logo.pngはアプリケーションの/logo.pngで画像を提供します。これがsrcの値になります。

警告: TypeScriptを使用している場合、srcプロパティにアクセスすると型エラーが発生する可能性があります。今のところは安全に無視できます。このガイドの終わりまでに修正されます。

ステップ7: 環境変数の移行

Next.jsはViteと同様に.env環境変数をサポートしています。主な違いは、クライアントサイドで環境変数を公開するために使用されるプレフィックスです。

  • VITE_プレフィックスのあるすべての環境変数をNEXT_PUBLIC_に変更してください。

Viteは特別なimport.meta.envオブジェクト上でいくつかの組み込み環境変数を公開していますが、これらはNext.jsではサポートされていません。次のように使用法を更新する必要があります:

  • import.meta.env.MODEprocess.env.NODE_ENV
  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'
  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'
  • import.meta.env.SSRtypeof window !== 'undefined'

Next.jsも組み込みのBASE_URL環境変数を提供していません。ただし、必要であれば、自分で設定できます:

  1. .envファイルに以下を追加:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. next.config.mjsファイルでbasePathprocess.env.NEXT_PUBLIC_BASE_PATHに設定:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // シングルページアプリケーション(SPA)を出力
  distDir: './dist', // ビルド出力ディレクトリを`./dist/`に変更
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // ベースパスを`/some-base-path`に設定
}

export default nextConfig
  1. import.meta.env.BASE_URLの使用箇所をprocess.env.NEXT_PUBLIC_BASE_PATHに更新

ステップ8: package.jsonのスクリプトを更新

これで、Next.jsへの移行が成功したかどうかをテストするためにアプリケーションを実行できるはずです。ただし、その前に、package.jsonscriptsをNext.js関連のコマンドで更新し、.nextnext-env.d.ts.gitignoreに追加する必要があります:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

npm run devを実行し、http://localhost:3000を開いてください。Next.jsで動作するアプリケーションが表示されるはずです。

例: ViteアプリケーションをNext.jsに移行した動作例はこのプルリクエストを確認してください。

ステップ9: クリーンアップ

これで、Vite関連のアーティファクトをコードベースから削除できます:

  • main.tsxを削除
  • index.htmlを削除
  • vite-env.d.tsを削除
  • tsconfig.node.jsonを削除
  • vite.config.tsを削除
  • Viteの依存関係をアンインストール

次のステップ

すべてが計画通りに進んだ場合、現在はシングルページアプリケーションとして機能するNext.jsアプリケーションが動作しています。ただし、まだNext.jsの利点のほとんどを活用していませんが、これから段階的な変更を加えてすべての利点を得ることができます。次に行う可能性のあることを以下に示します: