コンテンツをスキップ

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 Middlewareを使用すると、リクエストが完了する前にサーバー上でコードを実行できます。これは、ユーザーが認証済み専用ページにアクセスした際に、認証されていないコンテンツがちらつくのを避けるために、ユーザーをログインページにリダイレクトする際に特に役立ちます。ミドルウェアは、実験や国際化にも役立ちます。

組み込みの最適化

画像フォント、およびサードパーティスクリプトは、アプリケーションのパフォーマンスに大きな影響を与えることがよくあります。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. ./dist/types/**/*.ts./next-env.d.tsinclude配列に追加します。
  3. ./node_modulesexclude配列に追加します。
  4. compilerOptions内のplugins配列{ "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サーバーコンポーネント)を含める必要があります。このファイルは、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には、デフォルトでメタ文字セットメタビューポートのタグがすでに含まれているため、これらを<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と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の拡張子を使用できます。

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

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

次に、クライアント側のみで実行される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に事前レンダリングされます。

クライアントサイドのみのアプリケーションを開始したいため、Next.jsを構成して、Appコンポーネント以下からの事前レンダリングを無効にできます。

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>タグを維持することで、アプリケーションの変更量を減らし、上記の問題を防ぐことができます。その後、必要に応じて<Image>コンポーネントに移行して、ローダーを設定することによる画像最適化、または自動画像最適化機能を備えたデフォルトのNext.jsサーバーに移行することによるメリットを活用できます。

  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://: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のほとんどの利点を活用できていませんが、ここから段階的に変更を加えてすべての利点を享受することができます。次に実行したいことは次のとおりです。