ViteからNext.jsへの移行方法
このガイドでは、既存のViteアプリケーションをNext.jsに移行する方法を説明します。
なぜ移行するのか?
ViteからNext.jsに移行したい理由はいくつかあります:
初期ページの読み込み時間が遅い
デフォルトのViteプラグイン for Reactを使用してアプリケーションを構築した場合、アプリケーションは純粋なクライアントサイドアプリケーションです。クライアントサイドのみのアプリケーション、つまりシングルページアプリケーション(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の設定ファイルを作成する
プロジェクトのルートに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": truejsxをpreserveに設定します:"jsx": "preserve"allowJsをtrueに設定します:"allowJs": trueforceConsistentCasingInFileNamesをtrueに設定します:"forceConsistentCasingInFileNames": trueincrementalを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 レイアウトファイルが含まれている必要があります。このファイルは、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はメタデータ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のフレームワークに組み込まれた規約ベースのアプローチ(メタデータAPI)に移行しました。このアプローチにより、ページのSEOとウェブの共有性をより簡単に向上させることができます。
ステップ5: エントリーポイントページを作成する
Next.jsでは、page.tsxファイルを作成することでアプリケーションのエントリーポイントを宣言します。このファイルのViteでの最も近い相当物はmain.tsxファイルです。このステップでは、アプリケーションのエントリーポイントを設定します。
appディレクトリに[[...slug]]ディレクトリを作成します。
このガイドでは、まずNext.jsをSPA(シングルページアプリケーション)として設定することを目指しているため、アプリケーションのすべてのルートをキャッチするページエントリーポイントが必要です。そのために、appディレクトリに新しい[[...slug]]ディレクトリを作成します。
このディレクトリは、オプションの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>コンポーネントで直接使用することも、既存の<img>タグでオブジェクトのsrcプロパティを使用することもできます。
<Image>コンポーネントには、自動画像最適化の利点があります。<Image>コンポーネントは、画像の寸法に基づいて、結果の<img>のwidthおよびheight属性を自動的に設定します。これにより、画像の読み込み時のレイアウトシフトを防ぎます。ただし、アプリケーションに寸法の一方のみがスタイル設定され、他方がautoにスタイル設定されていない画像が含まれている場合、問題が発生する可能性があります。autoにスタイル設定されていない場合、寸法は<img>の寸法属性の値にデフォルト設定され、画像が歪んで表示される可能性があります。
<img>タグを保持することで、アプリケーションの変更量を減らし、上記の問題を防ぐことができます。その後、ローダーを設定することで画像を最適化するために<Image>コンポーネントに移行するか、または自動画像最適化を備えたデフォルトのNext.jsサーバーに移行することができます。
/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値になります。
Warning: TypeScriptを使用している場合、
srcプロパティにアクセスする際に型エラーが発生する可能性があります。これらは今のところ無視しても安全です。このガイドの最後までに修正されます。
ステップ7: 環境変数を移行する
Next.jsは、Viteと同様に.env環境変数をサポートしています。主な違いは、クライアントサイドで環境変数を公開するために使用されるプレフィックスです。
VITE_プレフィックスの環境変数をすべてNEXT_PUBLIC_に変更します。
Viteは、Next.jsではサポートされていない特別なimport.meta.envオブジェクトでいくつかの組み込み環境変数を公開します。それらの使用を次のように更新する必要があります:
import.meta.env.MODE⇒process.env.NODE_ENVimport.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でアプリケーションが実行されているのが確認できるはずです。
Example: 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の設定を更新する