Reactのエコシステムが大きく進化し、Next.jsのApp Routerが標準となってから久しいですが、皆さんは「データフェッチ」の設計で迷っていませんか?
「Server Componentsで取ればいいんでしょ?」という単純な話から一歩踏み込むと、キャッシュ戦略、ローディングUI(Suspense)、そしてSEOを意識したSSR(Server Side Rendering)のバランスなど、考慮すべき点は山ほどあります。
今回は、実案件での「ハマりポイント」をベースに、Next.js App Routerにおけるデータフェッチの最適化手法を徹底解説します。
1. なぜ「データフェッチ」の設計が重要なのか?
従来のPages Routerでは、getServerSideProps や getStaticProps を使って「ページ単位」でデータを取得していました。しかし、App Routerでは 「コンポーネント単位」 でデータを取得するのが基本思想です。
これにより、以下のメリットが得られます。
- データの局所化: 必要なデータが必要なコンポーネントに閉じ込めることができる。
- ウォーターフォール問題の解消:
Promise.allやSuspenseを活用した並列取得の制御。 - バンドルサイズの削減: クライアントに送るJavaScriptを最小限に抑えられる。
しかし、無計画にフェッチを散りばめると、パフォーマンスの低下やデバッグの難易度上昇を招きます。
2. 実践:Server Componentsでの効率的なフェッチ
まずは、基本となるServer Componentsでのフェッチ方法を見てみましょう。Next.jsは標準の fetch APIを拡張しており、デフォルトで強力なキャッシュ機能を備えています。
TypeScript
// services/api.ts
export async function getProjectDetails(id: string) {
// Next.jsの拡張fetch。revalidateでISR(Incremental Static Regeneration)を実現
const res = await fetch(`https://api.example.com/projects/${id}`, {
next: { revalidate: 3600 }, // 1時間キャッシュ
});
if (!res.ok) {
// 現場のリアル:エラーハンドリングを怠ると、サイト全体が落ちる原因に
throw new Error('Failed to fetch project data');
}
return res.json();
}
【ここにNext.jsのデータキャッシュフロー図のスクショを貼る】
技術選定の理由:なぜ「標準fetch」を使うのか?
Axiosなどのライブラリも便利ですが、Next.js環境では標準の fetch を推奨します。理由は単純で、Next.jsのフルスタックなキャッシュ機能(Data Cache, Full Route Cache)と密接に統合されているからです。ライブラリを導入してバンドルサイズを増やすよりも、プラットフォームの機能を最大限に活かすのが「プロの選定」です。
3. 現場のハマりポイント:リクエスト・ウォーターフォール
コンポーネントを分割しすぎると、意図せず「Aが終わってからBが始まる」という直列処理(ウォーターフォール)が発生し、ユーザーの待機時間が増大します。
失敗例:直列フェッチ
TypeScript
// 悪い例:コンポーネント内でawaitを並べてしまう
async function Dashboard() {
const user = await getUser(); // ここで待機
const posts = await getPosts(user.id); // userが取れるまで始まらない
// ...
}
解決策:並列フェッチと Promise.all
TypeScript
async function Dashboard() {
const userPromise = getUser();
const postsPromise = getPosts();
// 両方のリクエストを同時に開始
const [user, posts] = await Promise.all([userPromise, postsPromise]);
// ...
}
4. ユーザー体験(UX)を最大化する「Streaming」と「Suspense」
すべてのデータが揃うまで画面が真っ白……そんな「有用性の低い(体験の悪い)」ページを避けるために、Streaming を活用しましょう。
コンポーネント単位でのローディング
時間のかかる重い処理(例:外部APIの集計結果)だけを Suspense で囲むことで、ページの一部を先に表示させることができます。
TypeScript
import { Suspense } from 'react';
import SkeletonCard from './components/SkeletonCard';
export default function Page() {
return (
<main>
<h1>ダッシュボード</h1>
{/* 軽いコンテンツはすぐ表示 */}
<ProfileOverview />
{/* 重いコンテンツは準備ができるまでスケルトンを表示 */}
<Suspense fallback={<SkeletonCard />}>
<HeavyAnalyticsComponent />
</Suspense>
</main>
);
}
【ここにスケルトンスクリーンが表示されている実際のブラウザ画面のスクショを貼る】
5. 現場のリアル:キャッシュの「罠」とパージ戦略
App Routerを使っていると、**「データを更新したのに画面が変わらない!」**という問題に必ず直面します。これはNext.jsの強力すぎるキャッシュが原因です。
解決策:revalidatePath と revalidateTag
データの更新(Mutation)を行った後は、明示的にキャッシュを破棄する必要があります。
TypeScript
// app/actions.ts (Server Actions)
'use server'
import { revalidatePath } from 'next/cache';
export async function updatePost(formData: FormData) {
// DB更新処理...
// 更新後に該当ページのキャッシュをパージ
revalidatePath('/posts/[id]');
}
ここでハマった!実際のトラブル事例
あるプロジェクトで、revalidatePath('/') を乱用した結果、トップページのビルドが頻発し、サーバー負荷が急増したことがありました。必要なのは「全削除」ではなく、タグベースのピンポイントなパージ(revalidateTag)です。これを意識するだけで、インフラコストを大幅に削減できます。
6. まとめ:モダン開発の「正解」を追い求める
Next.js App Routerは、強力な武器ですが、正しく扱わなければ「オーバースペックで遅いサイト」になりかねません。
- Server Componentsを基本にし、クライアントJavaScriptを減らす。
Promise.allで並列化を意識し、ウォーターフォールを防ぐ。Suspenseを使って「体感速度」を向上させる。- キャッシュ戦略を明文化し、データの整合性を保つ。
「有用性」とは、単に文字が並んでいることではなく、ユーザーがストレスなく、正確な情報を素早く得られることです。技術的なリファクタリングを通じて、あなたのブログも「最高の読書体験」を提供できるはずです。