Skip to content

Next.js App Routerでのデータフェッチ最適化戦略:パフォーマンスと開発体験のトレードオフをどう解くか

| 📖 8分で読めます
Next.js App Routerでのデータフェッチ最適化戦略:パフォーマンスと開発体験のトレードオフをどう解くか

Reactのエコシステムが大きく進化し、Next.jsのApp Routerが標準となってから久しいですが、皆さんは「データフェッチ」の設計で迷っていませんか?

「Server Componentsで取ればいいんでしょ?」という単純な話から一歩踏み込むと、キャッシュ戦略、ローディングUI(Suspense)、そしてSEOを意識したSSR(Server Side Rendering)のバランスなど、考慮すべき点は山ほどあります。

今回は、実案件での「ハマりポイント」をベースに、Next.js App Routerにおけるデータフェッチの最適化手法を徹底解説します。


1. なぜ「データフェッチ」の設計が重要なのか?

従来のPages Routerでは、getServerSidePropsgetStaticProps を使って「ページ単位」でデータを取得していました。しかし、App Routerでは 「コンポーネント単位」 でデータを取得するのが基本思想です。

これにより、以下のメリットが得られます。

  • データの局所化: 必要なデータが必要なコンポーネントに閉じ込めることができる。
  • ウォーターフォール問題の解消: Promise.allSuspense を活用した並列取得の制御。
  • バンドルサイズの削減: クライアントに送る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の強力すぎるキャッシュが原因です。

解決策:revalidatePathrevalidateTag

データの更新(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は、強力な武器ですが、正しく扱わなければ「オーバースペックで遅いサイト」になりかねません。

  1. Server Componentsを基本にし、クライアントJavaScriptを減らす。
  2. Promise.all で並列化を意識し、ウォーターフォールを防ぐ。
  3. Suspense を使って「体感速度」を向上させる。
  4. キャッシュ戦略を明文化し、データの整合性を保つ。

「有用性」とは、単に文字が並んでいることではなく、ユーザーがストレスなく、正確な情報を素早く得られることです。技術的なリファクタリングを通じて、あなたのブログも「最高の読書体験」を提供できるはずです。

デジタル体験を、
もっと美しく、機能的に。

Studio Puff では、デザインと技術を融合させた Webサイト制作・システム開発を行っています。
新規プロジェクトのご相談や、技術的な課題解決など お気軽にお問い合わせください。