Vite からの移行

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

移行する理由

Vite から Next.js に切り替えるべき理由はいくつかあります:

  1. 初期ページ読み込みの遅さ: React 向けのデフォルト Vite プラグイン でアプリケーションを構築した場合、そのアプリケーションは純粋なクライアントサイドアプリケーションとなります。クライアントサイドのみのアプリケーション(SPA: シングルページアプリケーション)は、初期ページの読み込みが遅くなる傾向があります。これには以下の理由があります:
    1. ブラウザは、データ読み込みリクエストを送信できるようになる前に、React コードとアプリケーション全体のバンドルをダウンロードして実行する必要があります。
    2. 新機能や依存関係を追加するたびにアプリケーションコードが肥大化します。
  2. 自動コード分割の欠如: 読み込みの遅さという問題は、コード分割である程度管理できます。しかし、手動でコード分割を試みると、パフォーマンスが悪化することがよくあります。手動でのコード分割では、ネットワークウォーターフォールを意図せず導入してしまう可能性があります。Next.js にはルーターに組み込まれた自動コード分割機能があります。
  3. ネットワークウォーターフォール: パフォーマンス低下の一般的な原因は、アプリケーションがデータを取得するためにクライアントとサーバー間で順次リクエストを行うことです。SPA でのデータ取得の一般的なパターンは、最初にプレースホルダーをレンダリングし、コンポーネントがマウントされた後にデータを取得することです。残念ながら、これにより、データを取得する子コンポーネントは、親コンポーネントが自身のデータの読み込みを完了するまでデータ取得を開始できません。Next.js では、この問題は解決されています。サーバーコンポーネントでデータを取得することで解決します。
  4. 高速で意図的なローディング状態: Suspense を使ったストリーミング の組み込みサポートにより、Next.js では、UI のどの部分を最初に、どの順序で読み込むかをより意図的に制御できます。これにより、ネットワークウォーターフォールを導入せずに、読み込みが速く レイアウトシフト のないページを構築できます。
  5. データ取得戦略の選択: 必要に応じて、Next.js ではページやコンポーネントごとにデータ取得戦略を選択できます。ビルド時、サーバーでのリクエスト時、またはクライアントでデータを取得するかを決定できます。例えば、CMS からデータを取得し、ブログ投稿をビルド時にレンダリングして、CDN に効率的にキャッシュすることができます。
  6. ミドルウェア: Next.js ミドルウェア を使用すると、リクエストが完了する前にサーバー上でコードを実行できます。これは、認証が必要なページにユーザーがアクセスした際に、認証されていないコンテンツが一瞬表示されるのを防ぎ、ログインページにリダイレクトする場合などに特に有用です。ミドルウェアは、実験や国際化にも役立ちます。
  7. 組み込みの最適化: 画像、フォント、サードパーティスクリプトは、アプリケーションのパフォーマンスに大きな影響を与えることがよくあります。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 サーバーコンポーネント)を含める必要があります。このファイルは app ディレクトリの最上位レベルで定義されます。

Vite アプリケーションでルートレイアウトファイルに最も近いのは、<html>, <head>, <body> タグを含む index.html ファイル です。

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

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

豆知識: レイアウトファイルには .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 ファイルを作成し、以下の内容を記述します:
'use client'

import dynamic from 'next/dynamic'
import '../../index.css'

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

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

import dynamic from 'next/dynamic'
import '../../index.css'

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

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

豆知識: ページファイルには .js, .jsx, .tsx のいずれの拡張子も使用できます。

このファイルには、'use client' ディレクティブによって クライアントコンポーネント としてマークされた <Page> コンポーネントが含まれています。このディレクティブがない場合、コンポーネントは サーバーコンポーネント になります。

Next.js では、クライアントコンポーネントはクライアントに送信される前にサーバー上で HTML にプリレンダリングされます。しかし、最初は純粋なクライアントサイドアプリケーションにしたいため、<App> コンポーネントのプリレンダリングを無効にする必要があります。そのために、ssr オプションを false に設定して動的にインポートします:

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

Step 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> の dimension 属性の値にデフォルト設定され、画像が歪んで表示される可能性があります。

<img> タグを維持すると、アプリケーションの変更量を減らし、上記の問題を防ぐことができます。ただし、自動最適化を活用するために、後で <Image> コンポーネントに移行することをお勧めします。

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

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

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

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

Step 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 に更新

Step 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

npm run dev を実行し、http://localhost:3000 を開きます。アプリケーションが Next.js で動作しているのが確認できるはずです。

アプリケーションが従来の Vite 設定に従っていた場合、これだけで動作する Next.js アプリケーションが完成します。

例: Vite アプリケーションを Next.js に移行した実際の例は このプルリクエスト で確認できます。

Step 9: クリーンアップ

Vite 関連のファイルをコードベースから削除できます:

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

次のステップ

すべてが計画通りに進んだ場合、シングルページアプリケーションとして動作する Next.js アプリケーションが完成しています。ただし、まだ Next.js の利点のほとんどを活用できていないので、段階的な変更を加えてすべての利点を得ることができます。次に取り組むべきこと: