Vite からの移行
このガイドは、既存の Vite アプリケーションを Next.js に移行するのに役立ちます。
なぜ切り替えるのか?
Vite から Next.js に切り替えたい理由はいくつかあります:
初期ページの読み込み時間の遅さ
React 用デフォルト Vite プラグイン を使用してアプリケーションを構築した場合、アプリケーションは純粋にクライアント側のアプリケーションです。クライアント側のみのアプリケーション、つまり Single Page Application(SPA)では、初期ページの読み込み時間が遅くなることがあります。これは次のような理由で発生します:
- ブラウザーが、React コードとアプリケーション全体のバンドルをダウンロードして実行するのを待つ必要があるため、コードがデータをロードするためにリクエストを送信できるようになるまで遅れが生じます。
- 各新機能と追加の依存関係を追加するたびに、アプリケーションコードが成長します。
自動コード分割がない
前述の読み込み時間の遅さの問題は、コード分割である程度管理できます。ただし、コード分割を手動で試みると、パフォーマンスがさらに悪化することがよくあります。手動でコード分割を行うと、ネットワークのウォーターフォールを不注意に導入してしまうことがあります。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 の設定ファイルを作成する
プロジェクトの root に next.config.mjs
を作成します。このファイルには Next.js の設定オプションが含まれます。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // シングルページ アプリケーション(SPA)として出力します。
distDir: './dist', // ビルド出力ディレクトリを `./dist/` に変更します。
}
export default nextConfig
Good to know: Next.js の設定ファイルには
.js
または.mjs
のどちらも使用できます。
ステップ 3: TypeScript の設定を更新する
TypeScript を使用している場合、tsconfig.json
ファイルを次の変更で更新し、Next.js との互換性を持たせる必要があります。TypeScript を使用していない場合は、このステップをスキップできます。
tsconfig.node.json
へのプロジェクト参照を削除しますinclude
配列に./dist/types/**/*.ts
および./next-env.d.ts
を追加しますexclude
配列に./node_modules
を追加しますcompilerOptions
内のplugins
配列に{ "name": "next" }
を追加します:"plugins": [{ "name": "next" }]
esModuleInterop
をtrue
に設定します:"esModuleInterop": true
jsx
をpreserve
に設定します:"jsx": "preserve"
allowJs
をtrue
に設定します:"allowJs": true
forceConsistentCasingInFileNames
をtrue
に設定します:"forceConsistentCasingInFileNames": true
incremental
をtrue
に設定します:"incremental": true
以下は、これらの変更を加えた動作可能な 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: Root レイアウトを作成する
Next.js App Router アプリケーションは、アプリケーション内のすべてのページを包む root レイアウト ファイルを含める必要があります。これは React Server Component であり、app
ディレクトリのトップレベルで定義されています。
Vite アプリケーションでの root レイアウトファイルに最も近いものは、<html>
, <head>
, <body>
タグを含む index.html
ファイル です。
このステップでは、index.html
ファイルを root レイアウトファイルに変換します:
src
ディレクトリ内に新しいapp
ディレクトリを作成します。- その
app
ディレクトリ内に新しいlayout.tsx
ファイルを作成します:
- TypeScript
- JavaScript
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return '...'
}
export default function RootLayout({ children }) {
return '...'
}
Good to know:
.js
、.jsx
、または.tsx
の拡張子をレイアウトファイルに使用できます。
index.html
ファイルの内容を<RootLayout>
コンポーネントにコピーします。この際にbody.div#root
とbody.script
タグを<div id="root">{children}</div>
に置き換えます:
- TypeScript
- JavaScript
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>
)
}
export default function RootLayout({ children }) {
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>
)
}
- Next.js は既にデフォルトで meta charset および meta viewport タグを含んでいるので、これらを
<head>
から安全に削除できます:
- TypeScript
- JavaScript
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>
)
}
export default function RootLayout({ children }) {
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>
)
}
favicon.ico
,icon.png
,robots.txt
などの メタデータファイルは、それらをapp
ディレクトリのトップレベルに配置しておく限り、アプリケーションの<head>
タグに自動的に追加されます。サポートされているすべてのファイルをapp
ディレクトリに移動した後、その<link>
タグを安全に削除できます:
- TypeScript
- JavaScript
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>
)
}
export default function RootLayout({ children }) {
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>
)
}
- 最後に、Next.js は Metadata API を使用して
<head>
の最後のタグを管理できます。最終的なメタデータ情報をエクスポートされたmetadata
オブジェクト に移動します:
- TypeScript
- JavaScript
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>
)
}
export const metadata = {
title: 'My App',
description: 'My App is a...',
}
export default function RootLayout({ children }) {
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
ファイルです。このステップでは、アプリケーションのエントリーポイントを設定します。
app
ディレクトリ内に[[...slug]]
ディレクトリを作成します。
このガイドでは、最初に Next.js を SPA(Single Page Application)としてセットアップすることを目指しているため、アプリケーションのすべてのルートをキャッチするページエントリーポイントが必要です。そのために、新しい [[...slug]]
ディレクトリを app
ディレクトリ内に作成します。
このディレクトリは optional catch-all route segment と呼ばれます。Next.js はファイルシステムベースのルーターを使用しており、ディレクトリを使用してルートを定義します。この特別なディレクトリは、アプリケーションのすべてのルートがその中にある page.tsx
ファイルに導かれることを保証します。
app/[[...slug]]
ディレクトリ内に新しいpage.tsx
ファイルを作成し、次の内容にします:
- TypeScript
- JavaScript
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // これを更新します
}
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // これを更新します
}
Good to know:
.js
、.jsx
、または.tsx
の拡張子はページファイルに使用できます。
このファイルは Server Componentです。next build
を実行すると、このファイルは静的アセットにプリレンダリングされます。動的なコードは一切不要です。
このファイルはグローバルな CSS をインポートし、generateStaticParams
に対して、1つのルート、/
のインデックスルートのみを生成することを指示します。
さて、Vite アプリケーションの残りの部分をクライアント専用で動作させるようにしましょう。
- TypeScript
- JavaScript
'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'
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です。クライアントコンポーネントは、クライアントに送信される前にサーバーでHTMLにプリレンダリングされます。
クライアント専用アプリケーションを開始したいので、Next.js を構成して App
コンポーネントの下からプリレンダリングを無効にすることができます。
const App = dynamic(() => import('../../App'), { ssr: false })
次に、エントリーポイントページを新しいコンポーネントに更新します:
- TypeScript
- JavaScript
import '../../index.css'
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}
import '../../index.css'
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}
ステップ 6: 静的な画像のインポートを更新する
Next.js は Vite と少し異なる方法で静的画像のインポートを扱います。Vite では、画像ファイルをインポートすると、その公開 URL が文字列として返されます:
import image from './img.png' // `image` は本番では '/assets/img.2d8efhg.png' になります
export default function App() {
return <img src={image} />
}
Next.js では、静的画像のインポートはオブジェクトを返します。このオブジェクトは Next.js の <Image>
コンポーネントで直接使用することができるか、オブジェクトの src
プロパティを使用して既存の <img>
タグと一緒に使用することができます。
<Image>
コンポーネントには自動画像最適化の追加の利点があります。<Image>
コンポーネントは、結果として得られる <img>
の width
と height
属性を画像の寸法に基づいて自動的に設定します。これにより、画像の読み込み時にレイアウトシフトを防ぎます。ただし、アプリケーションに寸法のうち1つだけがスタイル設定され、他が auto
にスタイル設定されていない画像が含まれている場合、問題が発生する可能性があります。auto
にスタイル設定されていない場合、寸法はデフォルトで <img>
の寸法属性の値になるため、画像が歪んで表示される可能性があります。
<img>
タグを保持することで、アプリケーション内の変更箇所を減らし、上記の問題を防ぐことができます。その後、画像を最適化するためにローダーを設定することでまたは自動画像最適化を持つデフォルトの Next.js サーバーに移行することにより、後で <Image>
コンポーネントに移行することをオプションで選択できます。
- 画像のインポートパスを絶対パスから
/public
への相対インポートに変換します:
// Before
import logo from '/logo.png'
// After
import logo from '../public/logo.png'
- 画像オブジェクト全体ではなく、画像の
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 は特別な import.meta.env
オブジェクトにいくつかの組み込みの環境変数を公開しますが、Next.js ではサポートされていません。それらの使用法を次のように更新する必要があります:
import.meta.env.MODE
⇒process.env.NODE_ENV
import.meta.env.PROD
⇒process.env.NODE_ENV === 'production'
import.meta.env.DEV
⇒process.env.NODE_ENV !== 'production'
import.meta.env.SSR
⇒typeof window !== 'undefined'
Next.js は組み込みの BASE_URL
環境変数も提供していません。ただし、必要に応じて設定することができます:
- 次を
.env
ファイルに追加します:
# ... \{#}
NEXT_PUBLIC_BASE_PATH="/some-base-path"
- 次のように
next.config.mjs
ファイルでbasePath
をprocess.env.NEXT_PUBLIC_BASE_PATH
に設定します:
/** @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
import.meta.env.BASE_URL
の使用をprocess.env.NEXT_PUBLIC_BASE_PATH
に更新します
ステップ 8: package.json
のスクリプトを更新する
次に、Next.js に正常に移行できたかをテストするためにアプリケーションを実行できるようにします。しかしその前に、package.json
の scripts
を Next.js 関連のコマンドで更新し、.next
および next-env.d.ts
を .gitignore
に追加する必要があります:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
# ... \{#}
.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 の多くの利点を活用しているわけではありません。段階的な変更を行い、すべての利点を享受できるようにすることができます。次に行うことを検討したいのは次のとおりです:
- React Router から Next.js App Routerに移行して以下を取得する:
<Image>
コンポーネントで画像を最適化するnext/font
でフォントを最適化する<Script>
コンポーネントでサードパーティのスクリプトを最適化する- Next.js の規則をサポートするために ESLint の設定を更新する