Menu

Viteからの移行

このガイドは、既存の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ミドルウェアにより、リクエストが完了する前にサーバー上でコードを実行できます。これは、認証済みページにアクセスした際に未認証のコンテンツが一瞬表示されるのを避けるため、ユーザーをログインページにリダイレクトする場合に特に便利です。ミドルウェアは実験や国際化にも役立ちます。

組み込みの最適化

画像フォントサードパーティスクリプトは、アプリケーションのパフォーマンスに大きな影響を与えることがよくあります。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" } を追加
  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 ファイルを作成します:
app/layout.tsx
TypeScript
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}

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

  1. 以前作成した <RootLayout> コンポーネントに index.html ファイルの内容をコピーし、body.div#rootbody.script タグを <div id="root">{children}</div> に置き換えます:
app/layout.tsx
TypeScript
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タグを含んでいるため、<head> からこれらを安全に削除できます:
app/layout.tsx
TypeScript
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
TypeScript
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はMetadata APIを使用して最後の <head> タグを管理できます。最終的なメタデータ情報をmetadata オブジェクトにエクスポートします:
app/layout.tsx
TypeScript
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のフレームワークに組み込まれた規約ベースのアプローチ(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 ファイルを作成します:
app/[[...slug]]/page.tsx
TypeScript
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // これは後で更新します
}

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

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

このファイルはグローバルCSSをインポートし、generateStaticParamsに、インデックスルート / のみを生成することを伝えます。

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

app/[[...slug]]/client.tsx
TypeScript
'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 })

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

app/[[...slug]]/page.tsx
TypeScript
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>タグを維持することで、アプリケーション内の変更点を減らし、上記の問題を防ぐことができます。その後、ローダーを設定するか、自動画像最適化を備えたデフォルトのNext.jsサーバーに移行することで、任意で後から<Image>コンポーネントに移行し、画像を最適化できます。

  1. /publicからインポートされた画像の絶対インポートパスを相対インポートに変換します:
// 変更前
import logo from '/logo.png'
 
// 変更後
import logo from '../public/logo.png'
  1. <img>タグに画像オブジェクト全体ではなく、srcプロパティを渡します:
// 変更前
<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.jsonのスクリプトを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でアプリケーションが実行されているはずです。

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

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

これで、Viteに関連する成果物をコードベースからクリーンアップできます:

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

次のステップ

計画通りに進めば、シングルページアプリケーションとして実行されるNext.jsアプリケーションが稼働しているはずです。ただし、まだNext.jsの利点のほとんどを活用できていませんが、今後段階的に変更を加えることで、すべての利点を得ることができます。次に行うことは以下のようなものです: