Vite から Next.js への移行
このガイドは既存のViteアプリケーションをNext.jsに移行する際に役立ちます。
なぜ移行するのか?
ViteからNext.jsに移行する理由はいくつかあります:
初期ページ読み込み時間が遅い
ReactのデフォルトViteプラグインでアプリケーションを構築した場合、そのアプリケーションは純粋なクライアントサイドのアプリケーションです。クライアントサイドのみのアプリケーション(シングルページアプリケーション/SPAとも呼ばれる)は、しばしば初期ページの読み込み時間が遅いという問題を抱えています。これは次のような理由によります:
- ブラウザはReactコードとアプリケーションバンドル全体がダウンロードされ実行されるのを待ってから、コードがデータを読み込むためのリクエストを送信できるようになります。
- アプリケーションコードは新機能や追加の依存関係によって増大していきます。
自動コード分割がない
前述の読み込み時間の問題は、コード分割によってある程度管理できます。しかし、手動でコード分割を行おうとすると、しばしばパフォーマンスが悪化します。手動でコード分割を行うと、ネットワークウォーターフォールを無意識に導入してしまうことがあります。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
を依存関係としてインストールすることです:
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
補足: Next.js設定ファイルには
.js
または.mjs
のいずれかを使用できます。
ステップ3:TypeScript設定を更新する
TypeScriptを使用している場合は、Next.jsと互換性を持たせるためにtsconfig.json
ファイルを次のように更新する必要があります。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:ルートレイアウトを作成する
Next.js Appルーターアプリケーションには、アプリケーション内のすべてのページをラップするルートレイアウトファイルが必要です。このファイルはReactサーバーコンポーネントであり、app
ディレクトリのトップレベルで定義されています。
Viteアプリケーションでルートレイアウトファイルに最も近いものは、<html>
、<head>
、<body>
タグを含むindex.html
ファイルです。
このステップでは、index.html
ファイルをルートレイアウトファイルに変換します:
src
ディレクトリ内に新しいapp
ディレクトリを作成します。- その
app
ディレクトリ内に新しいlayout.tsx
ファイルを作成します:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return '...'
}
補足: レイアウトファイルには
.js
、.jsx
、または.tsx
拡張子を使用できます。
index.html
ファイルの内容を、先ほど作成した<RootLayout>
コンポーネントにコピーし、body.div#root
とbody.script
タグを<div id="root">{children}</div>
に置き換えます:
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>
)
}
- Next.jsはデフォルトでmeta charsetとmeta viewportタグを含んでいるため、それらを
<head>
から安全に削除できます:
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>
)
}
favicon.ico
、icon.png
、robots.txt
などのメタデータファイルは、app
ディレクトリの最上位に配置されていれば、アプリケーションの<head>
タグに自動的に追加されます。サポートされているすべてのファイルをapp
ディレクトリに移動した後、それらの<link>
タグを安全に削除できます:
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>
)
}
- 最後に、Next.jsはメタデータAPIを使用して残りの
<head>
タグを管理できます。最終的なメタデータ情報をエクスポートされたmetadata
オブジェクトに移動します:
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
ファイルです。このステップでは、アプリケーションのエントリーポイントを設定します。
app
ディレクトリ内に[[...slug]]
ディレクトリを作成します。
このガイドでは、まずNext.jsをSPA(シングルページアプリケーション)として設定することを目指しているため、ページのエントリーポイントがアプリケーションのすべての可能なルートをキャッチする必要があります。そのために、app
ディレクトリ内に新しい[[...slug]]
ディレクトリを作成します。
このディレクトリはオプションのキャッチオールルートセグメントと呼ばれるものです。Next.jsはファイルシステムベースのルーターを使用しており、フォルダーを使用してルートを定義します。この特殊なディレクトリにより、アプリケーションのすべてのルートが含まれるpage.tsx
ファイルに確実に向けられます。
app/[[...slug]]
ディレクトリ内に、次の内容で新しいpage.tsx
ファイルを作成します:
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // これを更新します
}
補足: ページファイルには
.js
、.jsx
、または.tsx
拡張子を使用できます。
このファイルはサーバーコンポーネントです。next build
を実行すると、ファイルは静的アセットに事前レンダリングされます。動的コードは必要ありません。
このファイルはグローバルCSSをインポートし、generateStaticParams
に対して、/
のインデックスルートのみを生成することを伝えています。
次に、クライアントのみで実行されるViteアプリケーションの残りの部分を移行しましょう。
'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 })
ここで、エントリーポイントページを更新して新しいコンポーネントを使用します:
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>
タグを維持することで、アプリケーションの変更量を減らし、上記の問題を防ぐことができます。後で、ローダーを設定するか、自動画像最適化機能を持つデフォルトのNext.jsサーバーに移行して、<Image>
コンポーネントに移行して画像の最適化を活用することができます。
/public
からインポートされた画像の絶対インポートパスを相対インポートに変換します:
// 変更前
import logo from '/logo.png'
// 変更後
import logo from '../public/logo.png'
- 画像オブジェクト全体ではなく、画像の
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.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関連のコマンドで更新し、.gitignore
に.next
とnext-env.d.ts
を追加する必要があります:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
# ...
.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の利点のほとんどを活用していませんが、段階的に変更を加えてすべての利点を享受し始めることができます。次に行うべきことは以下の通りです:
- React RouterからNext.jsのAppルーターに移行して以下を得る:
- 自動コード分割
- ストリーミングサーバーレンダリング
- Reactサーバーコンポーネント
<Image>
コンポーネントで画像を最適化するnext/font
でフォントを最適化する<Script>
コンポーネントでサードパーティスクリプトを最適化する- Next.jsルールをサポートするようにESLint設定を更新する