Sponsor
ChatHubChatHub Use GPT-4, Gemini, Claude 3.5 and more chatbots side-by-side
ここをクリック
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" }を追加:"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ルーターアプリケーションには、アプリケーション内のすべてのページをラップするルートレイアウトファイルが必要です。このファイルは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. index.htmlファイルの内容を、先ほど作成した<RootLayout>コンポーネントにコピーし、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はメタデータ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の規約ベースのアプローチ(メタデータ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. 画像オブジェクト全体ではなく、画像の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関連のコマンドで更新し、.gitignore.nextnext-env.d.tsを追加する必要があります:

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の利点のほとんどを活用していませんが、段階的に変更を加えてすべての利点を享受し始めることができます。次に行うべきことは以下の通りです: