コンテンツにスキップ

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

このガイドでは、既存の Vite アプリケーションを Next.js に移行するのに役立ちます。

なぜ切り替えるのか?

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

初期ページ読み込み時間の遅延

アプリケーションを React 用のデフォルトの Vite プラグイン で構築した場合、アプリケーションは純粋なクライアントサイド アプリケーションになります。シングルページアプリケーション (SPA) とも呼ばれるクライアントサイドのみのアプリケーションは、初期ページ読み込み時間が遅くなることがよくあります。これはいくつかの理由で発生します。

  1. ブラウザは、コードがデータ読み込みのためのリクエストを送信できるようになる前に、React コードとアプリケーション全体のバンドルがダウンロードされ実行されるのを待つ必要があります。
  2. アプリケーション コードは、新しい機能や追加の依存関係を追加するたびに増えていきます。

自動コード分割がない

上記の読み込み時間遅延の問題は、コード分割である程度管理できます。ただし、手動でコード分割しようとすると、パフォーマンスが悪化することがよくあります。手動でコード分割すると、ネットワークのウォーターフォールを意図せず導入してしまう可能性があります。Next.js はルーターに自動コード分割が組み込まれています。

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

パフォーマンスの低下の一般的な原因は、アプリケーションがデータを取得するためにクライアントとサーバー間で順次リクエストを行う場合に発生します。SPA でのデータ取得の一般的なパターンは、プレースホルダーを初期レンダリングしてから、コンポーネントがマウントされた後にデータを取得することです。残念ながら、これは、データを取得する子コンポーネントが、親コンポーネントが自身のデータ読み込みを完了するまで、データの取得を開始できないことを意味します。

Next.js ではクライアントでのデータ取得はサポートされていますが、データ取得をサーバーにシフトすることもできます。これにより、クライアントとサーバーのウォーターフォールを排除できます。

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

React Suspense を介したストリーミング の組み込みサポートにより、ネットワークのウォーターフォールを導入することなく、UI のどの部分を最初に読み込み、どの順序で読み込むかをより意図的に行うことができます。

これにより、読み込みの速いページを構築し、レイアウトシフト を排除することができます。

データ取得戦略を選択する

ニーズに応じて、Next.js ではページおよびコンポーネントごとにデータ取得戦略を選択できます。ビルド時、サーバーでのリクエスト時、またはクライアントでの取得を選択できます。たとえば、CMS からデータを取得し、ブログ投稿をビルド時にレンダリングすることができます。これにより、CDN で効率的にキャッシュできます。

プロキシ

Next.js Proxy を使用すると、リクエストが完了する前にサーバーでコードを実行できます。これは、ユーザーが認証専用ページにアクセスしたときに、ログインページにリダイレクトすることで、認証されていないコンテンツのフラッシュを回避するのに特に役立ちます。プロキシは、実験や国際化にも役立ちます。

組み込みの最適化

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

移行手順

この移行の目標は、可能な限り迅速に動作する Next.js アプリケーションを取得することです。これにより、Next.js の機能を段階的に採用できるようになります。まず、既存のルーターを移行せずに、純粋なクライアントサイド アプリケーション (SPA) として維持します。これにより、移行プロセス中の問題の発生を最小限に抑え、マージ競合を減らすことができます。

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

最初に行う必要があるのは、`next` を依存関係としてインストールすることです。

ターミナル
npm install next@latest

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

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

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Outputs a Single-Page Application (SPA).
  distDir: './dist', // Changes the build output directory to `./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` を追加するinclude 配列
  3. `exclude` 配列に `./node_modules` を追加するexclude 配列
  4. `compilerOptions` の `plugins` 配列に `{ "name": "next" }` を追加するcompilerOptionsplugins 配列: `"plugins": [{ "name": "next" }]`
  5. `esModuleInterop` を `true` に設定するesModuleInterop: `"esModuleInterop": true`
  6. `jsx` を `react-jsx` に設定するjsx: `"jsx": "react-jsx"`
  7. `allowJs` を `true` に設定するallowJs: `"allowJs": true`
  8. `forceConsistentCasingInFileNames` を `true` に設定するforceConsistentCasingInFileNames: `"forceConsistentCasingInFileNames": true`
  9. `incremental` を `true` に設定するincremental: `"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": "react-jsx",
    "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` ファイル です。

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

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

補足: レイアウトファイルには、`.js`、`.jsx`、または `.tsx` 拡張子を使用できます。

  1. 以前に作成した `` コンポーネントに `index.html` ファイルの内容をコピーし、`.div#root` と `.script` タグを `
    {children}
    ` で置き換えます。
app/layout.tsx
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>
  )
}
  1. Next.js はデフォルトで meta charsetmeta viewport タグをすでに含んでいるため、`` からそれらを安全に削除できます。
app/layout.tsx
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>
  )
}
  1. favicon.icoicon.pngrobots.txt などの メタデータファイルは、app ディレクトリの最上位に配置されている限り、アプリケーションの <head> タグに自動的に追加されます。サポートされているすべてのファイルapp ディレクトリに移動した後、それらの <link> タグを安全に削除できます。
app/layout.tsx
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>
  )
}
  1. 最後に、Next.js は メタデータ API を使用して、最後の <head> タグを管理できます。最終的なメタデータ情報をエクスポートされた metadata オブジェクトに移動します。
app/layout.tsx
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>
  )
}

上記変更により、index.html ですべてを宣言するのではなく、フレームワークに組み込まれた Next.js の規約ベースのアプローチ(メタデータ API)を使用するようになりました。このアプローチにより、ページの SEO と Web での共有可能性をより簡単に向上させることができます。

ステップ 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` ファイルを作成し、次の内容を記述します。
app/[[...slug]]/page.tsx
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // We'll update this
}

補足: ページファイルには、`.js`、`.jsx`、または `.tsx` 拡張子を使用できます。

このファイルはServer Componentです。 `next build` を実行すると、ファイルは静的アセットに事前レンダリングされます。動的なコードは必要ありません。

このファイルはグローバル CSS をインポートし、`generateStaticParams` に、ルート `/` で 1 つのルートのみを生成することを伝えます。

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

app/[[...slug]]/client.tsx
'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'` ディレクティブによって定義されるClient Componentです。Client Components は、クライアントに送信される前にサーバーで HTML に事前レンダリングされます。

クライアント専用アプリケーションとして開始したいので、`App` コンポーネントから下への事前レンダリングを無効にするように Next.js を設定できます。

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

これで、新しいコンポーネントを使用するようにエントリーポイントページを更新します。

app/[[...slug]]/page.tsx
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` will be '/assets/img.2d8efhg.png' in production
 
export default function App() {
  return <img src={image} />
}

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

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

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

  1. /public からインポートした画像の絶対インポートパスを相対インポートに変換します。
// Before
import logo from '/logo.png'
 
// After
import logo from '../public/logo.png'
  1. <img> タグに画像オブジェクト全体ではなく、画像 src プロパティを渡します。
// Before
<img src={logo} />
 
// After
<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.MODE` ⇒ `process.env.NODE_ENV`
  • `import.meta.env.PROD` ⇒ `process.env.NODE_ENV === 'production'`
  • `import.meta.env.DEV` ⇒ `process.env.NODE_ENV !== 'production'`
  • `import.meta.env.SSR` ⇒ `typeof window !== 'undefined'`

Next.js は、組み込みの `BASE_URL` 環境変数も提供しません。ただし、必要に応じて設定できます。

  1. `.env` ファイルに以下を追加します。
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. `next.config.mjs` ファイルの `basePath` を `process.env.NEXT_PUBLIC_BASE_PATH` に設定します。
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Outputs a Single-Page Application (SPA).
  distDir: './dist', // Changes the build output directory to `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Sets the base path to `/some-base-path`.
}
 
export default nextConfig
  1. `import.meta.env.BASE_URL` の使用箇所を `process.env.NEXT_PUBLIC_BASE_PATH` に更新します。

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

これで、アプリケーションを実行して Next.js への移行が成功したかテストできるようになります。ただし、その前に、`package.json` の `scripts` を Next.js 関連のコマンドに更新し、`.next` と `next-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` を実行し、`https://: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 のメリットのほとんどを活用できていません。しかし、段階的な変更を開始して、すべてのメリットを享受できるようになりました。次に試すべきことは次のとおりです。