コンテンツへスキップ

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をインストールすることです

ターミナル
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を追加します
  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アプリケーションのルートレイアウトファイルに最も近いのは、<html><head><body>タグを含む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. 以前に作成した <RootLayout> コンポーネントに index.html ファイルの内容をコピーし、body.div#rootbody.script タグを <div id="root">{children}</div> で置き換えてください。
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 タグが含まれているため、<head> からこれらのタグを安全に削除できます。
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 は Metadata 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 の規約ベースのアプローチ(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
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // We'll update this
}

知っておくと便利: ページファイルには、.js.jsx、または .tsx 拡張子を使用できます。

このファイルは、サーバーコンポーネントです。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' ディレクティブで定義された クライアントコンポーネントです。クライアントコンポーネントは、クライアントに送信される前に、サーバー上で 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> コンポーネントに移行して、イメージの最適化を利用できます。

  1. /public からインポートしたイメージの絶対インポートパスを相対インポートに変換します
// Before
import logo from '/logo.png'
 
// After
import logo from '../public/logo.png'
  1. イメージオブジェクト全体ではなく、イメージの src プロパティを <img> タグに渡します
// 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 は、Next.js でサポートされていない特別な import.meta.env オブジェクトに、いくつかの組み込み環境変数を公開します。次のように使用法を更新する必要があります

  • 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', // 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.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 を実行し、https://127.0.0.1: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 のメリットのほとんどを活用していません。しかし、これから徐々に変更を加えて、すべてのメリットを享受できます。次に何をするかについて、いくつか提案します。