Menu

How to migrate from Vite to Next.js

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

なぜ切り替えるのか?

ViteからNext.jsへの切り替えを検討する理由はいくつかあります。

初期ページ読み込み時間が遅い

Vite向けのデフォルトReactプラグインでアプリケーションを構築している場合、アプリケーションは完全にクライアント側のアプリケーションです。クライアント側のみのアプリケーション(シングルページアプリケーション(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を依存関係としてインストールすることです。

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. compilerOptions内のplugins配列{ "name": "next" }を追加します。"plugins": [{ "name": "next" }]
  5. esModuleInteroptrueに設定します。"esModuleInterop": true
  6. jsxreact-jsxに設定します。"jsx": "react-jsx"
  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": "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アプリケーションのルートレイアウトファイルの最も近い同等物は、<html><head>、および<body>タグが含まれているindex.htmlファイルです。

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

  1. srcフォルダに新しいappディレクトリを作成します。
  2. そのディレクトリ内に新しい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はデフォルトでメタ文字セットおよびメタビューポートタグを含むため、<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拡張子を使用できます。

このファイルはServer Componentです。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'ディレクティブで定義されたClient Componentです。Client Componentはクライアントに送信される前にサーバー側で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>widthおよびheight属性を自動的に設定します。これにより、画像が読み込まれるときのレイアウトシフトを防止します。ただし、アプリケーションに一方のみがスタイル設定され、他方がautoにスタイル設定されていない画像が含まれている場合、これが問題を引き起こす可能性があります。autoにスタイル設定されていない場合、寸法は<img>寸法属性の値にデフォルト設定され、画像が歪んで表示される可能性があります。

<img>タグを保持することで、アプリケーション内の変更量が削減され、上記の問題が防止されます。その後、<Image>コンポーネントへの移行をオプションで選択して、ローダーを設定することで画像の最適化を利用することも、自動画像最適化を持つデフォルトのNext.jsサーバーに移行することもできます。

  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は、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', // シングルページアプリケーション(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関連コマンドで更新し、.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で実行されているアプリケーションが表示されます。

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

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

これでVite関連のアーティファクトからコードベースをクリーンアップできます。

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

次のステップ

すべてがうまくいったら、シングルページアプリケーションとして実行されている機能しているNext.jsアプリケーションが完成しました。ただし、Next.jsのほとんどの利点をまだ利用していませんが、すべての利点を得るために段階的な変更を開始できます。次にやるべきことは以下の通りです。