はじめに
この記事では、ReactフレームワークであるNext.jsの基礎的な概念について、App Routerを中心に解説します。
Next.jsはなぜ生まれたのか、どのようなメリットがあるのか、そしてApp Routerの根幹をなすServer/Client Componentやレンダリング戦略、キャッシュの仕組みについて図解を交えながら説明していきます。
これからNext.jsを学ぼうとしている初学者向けの記事となっています。
1. Next.js誕生の背景
Next.jsが登場する以前、Reactは主にクライアントサイドで動作するUIライブラリでした。そのため、アプリケーションを構築するには、ルーティング、データ取得、サーバーサイドでのレンダリング(SSR)といった機能を開発者が個別に選定し、設定する必要がありました。
特に、クライアントサイドレンダリング(CSR)のみで構築されたSPA(Single Page Application)には、以下のような課題がありました。
- 初期表示速度の遅延: ブラウザがJavaScriptファイルをダウンロードし、実行して初めてコンテンツが描画されるため、ユーザーが最初のコンテンツを目にするまでに時間がかかる。
- SEOへの懸念: 検索エンジンのクローラーがJavaScriptを実行しない場合、コンテンツが正しくインデックスされない可能性がある。
これらの課題を解決し、ReactによるWeb開発をよりパワフルで、かつ開発者にとって快適なものにするために、Vercel社によってNext.jsが開発されました。Next.jsは、サーバーサイドレンダリングや静的サイト生成(SSG)といった機能をフレームワークレベルで提供することで、パフォーマンスとSEOに優れたWebアプリケーションの構築を容易にしました。
Next.jsに馴染みがない方向け【Next.jsとReactの違い】
Next.jsとReactは密接な関係にありますが、その役割には明確な違いがあります。
React: UIを構築するための「ライブラリ」
Reactは、UI(ユーザーインターフェース)を構築するためのJavaScriptライブラリです。コンポーネントという部品を組み合わせてUIを作り、データの変化に応じて効率的にUIを更新することに特化しています。しかし、React単体では以下の機能は提供されません。
- ルーティング: 複数ページ間の画面遷移をどう実現するか
- データ取得: どこから、どのようにデータを取得するか
- ビルドシステム: コードをブラウザで実行できるように変換する方法
- レンダリング戦略: サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)など
これらの機能は、Reactでアプリケーションを開発する際に、開発者が別途ライブラリを選択し、設定する必要がありました。
Next.js: モダンなWebアプリケーション開発のための「フレームワーク」
一方、Next.jsはReactをベースにしたフレームワークです。Reactの強力なUI構築能力を基盤としつつ、現代のWebアプリケーション開発に必要な様々な機能や規約をあらかじめ提供しています。これにより、開発者は個別の設定に時間を費やすことなく、すぐにアプリケーション開発に着手できます。
Next.jsが提供する主な機能は以下の通りです。
- ファイルシステムベースのルーティング: ファイルやフォルダの配置によって自動的にルーティングが生成されます。
- サーバーサイドレンダリング (SSR) および 静的サイト生成 (SSG): 初期表示の高速化とSEO対策に効果的なレンダリング手法を簡単に導入できます。
- API ルート: バックエンドAPIを簡単に構築できます。
- 画像最適化: 画像の読み込みパフォーマンスを自動的に最適化します。
- 高速な開発体験: ホットリロードなど、開発効率を高める機能が充実しています。
2. Next.jsを使うメリット
Next.jsを採用することには、多くのメリットがあります。
- パフォーマンスとSEOの向上: SSRやSSGによって初期表示が高速化され、サーバーで生成されたHTMLが返されるため、クローラーがコンテンツを認識しやすくなりSEOに有利。
-
優れた開発者体験 (DX): ファイルシステムベースのルーティング(
app
ディレクトリにフォルダを作成するとそれがルートになる)や、ホットリロード、高速なビルドプロセスなど、開発を快適に進めるための機能が充実している。 - レンダリング戦略の柔軟性: ページやコンポーネントごとに、SSG、SSR、ISR、CSRを柔軟に選択・組み合わせることができる。
-
組み込み機能の充実: 画像最適化(
<Image>
コンポーネント)、フォント最適化、スクリプト最適化(<Script>
コンポーネント)、APIルートなど、モダンなWeb開発に必要な機能が標準で提供されている。
3. Server/Client Component
App Routerの導入により、Next.jsのコンポーネントはServer ComponentとClient Componentの2種類に大別されるようになりました。デフォルトはすべてServer Componentです。
Server Component
サーバーサイドでのみレンダリング(ReactコンポーネントをHTMLに変換する処理)されるコンポーネントです。主な特徴は以下の通りです。
- サーバー上でのみ実行: データフェッチやデータベースへの直接アクセスなど、従来はサーバーサイドでしか行えなかった処理をコンポーネント内に直接記述できる。
- バンドルサイズ削減: Server Componentのコードはクライアントに送信されるJavaScriptバンドルに含まれないため、クライアントの負荷が減り、初期表示が高速になる。
-
状態管理(Hooks)やブラウザAPIの利用不可:
useState
やuseEffect
といったフックや、window
、localStorage
などのブラウザAPIは使用できない。
// DBから直接データを取得する例
import { db } from '@/lib/db';
export default async function Page() {
const posts = await db.post.findMany();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Client Component
ファイルの先頭に"use client"
を記述することで定義されるコンポーネントです。従来のReactコンポーネントと同じように動作します。
-
インタラクティブなUIを構築:
useState
による状態管理、useEffect
によるライフサイクルイベントの処理、onClick
などのイベントハンドラを使用でき、ユーザーの操作に応じた動的なUIを実装できる。 -
ブラウザAPIの利用可能:
window
オブジェクト、localStorage
など、ブラウザ環境に依存するAPIを使用できる。
// Client Componentの例
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
コンポーネントの組み合わせ
Server Componentの中にClient Componentをインポートして利用することは可能です。ただし、Client Componentの中にServer Componentを直接インポートすることはできません(Server Componentはサーバーでしか動かないため)。Client Compenentの子コンポーネントはすべてClient Componentと化します(Client Boundary)。例外として、Server Componentをchildren propsとしてClient Componentに渡すことはできます。
3. レンダリング戦略
Next.jsでは、リクエストに応じてコンテンツをどのように生成するかを制御できます。
大きく分けると、サーバーで実行されるStatic RenderingとDynamic Rendering、クライアントで実行されるClient Side Renderingがあります。
Static Rendering (静的レンダリング)
- タイミング: ビルド時にHTMLを生成。
-
仕組み: データ取得を伴う場合、
fetch
リクエストがデフォルトでキャッシュ(cache: 'force-cache'
)。ビルド時に取得したデータを使ってHTMLを事前に生成し、CDNにキャッシュ。 - メリット: リクエスト時にはすでにHTMLが完成しているため、非常に高速にページを表示できる。
- ユースケース: ブログ記事、製品ページ、ドキュメントなど、内容が頻繁に更新されないページに適している。
// デフォルトでStatic Renderingになる
async function getData() {
// fetchのデフォルトは cache: 'force-cache'
const res = await fetch('https://api.example.com/...');
return res.json();
}
export default async function Page() {
const data = await getData();
// ...
}
Dynamic Rendering (動的レンダリング)
- タイミング: リクエスト時(ユーザーがページにアクセスした時)にサーバーでHTMLを生成。
-
仕組み:
fetch
でcache: 'no-store'
を指定したり、cookies()
やheaders()
といった動的な関数を使用すると、そのルートは自動的にDynamic Renderingになる。 - メリット: 常に最新の情報をユーザーに表示できる。パーソナライズされたコンテンツの提供が可能。
- ユースケース: ユーザーダッシュボード、ECサイトのショッピングカート、SNSのタイムラインなど、データが頻繁に更新される、またはユーザーごとに内容が異なるページに適している。
// Dynamic Renderingになる例
import { cookies } from 'next/headers';
async function getData() {
// fetchのキャッシュを無効化
const res = await fetch('https://api.example.com/...', { cache: 'no-store' });
return res.json();
}
export default async function Page() {
const cookieStore = cookies(); // 動的関数を使うとDynamic Renderingになる
const data = await getData();
// ...
}
Client Side Rendering (CSR)
- タイミング: ブラウザ(クライアント)でJavaScriptが実行された後。
- 仕組み: Client Component内でuseEffectなどを使ってクライアントから直接APIを叩き、取得したデータでUIを更新します。
- ユースケース: 初期表示のSEOが重要でなく、かつ頻繁なデータ更新が必要なUI部分(例: ユーザープロフィールの編集フォーム、検索結果のフィルタリング機能など)に使用されます。
"use client";
import { useState, useEffect } from 'react';
export default function Page() {
const [post, setPost] = useState(null);
useEffect(() => {
// コンポーネントのマウント後にデータを取得
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
.then(data => {
setPost(data);
});
}, []);
return (
<div>
{post && (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)}
</div>
);
}
5. データフェッチとレンダリング
Next.jsのデータフェッチ戦略は、前述のレンダリング方法と密接に関連しています。
各戦略のデータフェッチのタイミングとレンダリング方法を見ていきます。
SSG (Static Site Generation)
これはStatic Renderingに分類されます。ビルド時にデータを取得し、静的なHTMLを生成します。
SSR (Server-Side Rendering)
これはDynamic Renderingに分類されます。リクエストごとにサーバーでデータを取得し、動的にHTMLを生成します。
ISR (Incremental Static Regeneration)
ISRは、SSGの利点(高速表示)とSSRの利点(データの鮮度)を両立させるための仕組みです。
fetch
のnext.revalidate
オプションに秒数を指定することで実現します。ビルド時に一度静的ページを生成しますが、指定した秒数が経過した後にアクセスがあった場合、バックグラウンドでページの再生成が行われます。ユーザーにはまずキャッシュされた古いページが表示され、再生成が完了するとページが更新されます。
結果だけ見ると生成されたファイルを表示するのでStatic Renderingに分類されます。
// 60秒ごとにデータを再検証(ISR)
async function getData() {
const res = await fetch('https://api.example.com/...', {
next: { revalidate: 60 },
});
return res.json();
}
export default async function Page() {
const data = await getData();
// ...
}
この例では、ページは60秒間静的に配信され、60秒経過後の次のリクエストでデータが再検証され、ページが更新されます。これにより、サーバーへの負荷を抑えつつ、リアルタイムなコンテンツ更新が可能です。
CSR (Client Side Rendering)
サーバーからは動的なデータが含まれないHTMLを受け取り、データフェッチ及びレンダリングをクライアントで実行します。
6. キャッシュ
Next.jsはパフォーマンスを最大化するために、複数のレイヤーでキャッシュ機構を備えています。
Data Cache (データキャッシュ)
- 場所: サーバーサイド
-
役割:
fetch
リクエストの結果を永続的にキャッシュ。ビルドサーバーやFunctionサーバー間で共有されることもある。 -
制御:
fetch
のcache
オプションやrevalidate
オプションで制御。-
cache: 'force-cache'
: キャッシュを強制的に利用(デフォルト)。 -
cache: 'no-store'
: キャッシュを一切利用しない(Dynamic Renderingになる)。 -
next: { revalidate: <秒数> }
: 時間ベースの再検証(ISR)を行う。
-
Full Route Cache (フルトルートキャッシュ)
- 場所: サーバーサイド
- 役割: Server Componentのレンダリング結果(RSC PayloadとHTML)を丸ごとキャッシュ。
- 特徴: 静的にレンダリング可能なルートに対して自動的に適用される。fetchでキャッシュを無効化(Dynamic Rendering)すると、このFull Route Cacheも自動的に無効になる。リクエストに対して即座にキャッシュされたHTMLを返すため、極めて高速。
Request Memorization (リクエストのメモ化)
- 場所: サーバーサイド(Reactの機能)
- 役割: 単一のリクエスト〜レスポンスのライフサイクル内で、同じURLに対するfetchリクエストを重複して実行しないようにする。
- 特徴: 例えば、レイアウトとページの両方で同じfetchを呼び出しても、実際のネットワークリクエストは一度しか発生しない。これはキャッシュというより、重複実行を防ぐ「重複排除」の仕組み。
Router Cache (ルーターキャッシュ)
- 場所: クライアントサイド(ブラウザのメモリ)
- 役割: 一度訪れたルートのRSC Payload(Server Componentのレンダリング結果)をキャッシュ。
- 特徴: ユーザーがブラウザの「戻る/進む」ボタンでナビゲートしたり、コンポーネントでページ間を移動したりする際に、サーバーにリクエストを送ることなく、キャッシュから即座にページを表示。これにより、非常にスムーズな画面遷移が実現される。
これらのキャッシュは連携して動作し、Next.jsアプリケーションの高速化に貢献しています。
まとめ
Next.jsのApp Routerは、Server Componentを基本とし、用途に応じてレンダリング戦略やキャッシュを柔軟に制御することで、最高のパフォーマンスと開発者体験を提供します。これらの基本的な概念を理解することが、Next.jsを効果的に活用するための第一歩となります!
習いたてのタイミングでは、どの場面で何を活用するかを判断するのが難しいと思うのでたくさん触って検証するのが良いと思います!
参考文献