T-CREATOR

Next.js Server Components 時代のデータ取得戦略:fetch キャッシュと再検証の新常識

Next.js Server Components 時代のデータ取得戦略:fetch キャッシュと再検証の新常識

Next.js 13 以降、App Router と Server Components の登場により、データ取得の方法が大きく変わりました。従来の getServerSidePropsgetStaticProps に代わり、コンポーネント内で直接 fetch を使ったデータ取得が可能になりましたが、それに伴いキャッシュと再検証の仕組みも刷新されています。本記事では、Server Components 時代における fetch のキャッシュ戦略と再検証の新しい考え方について、初心者の方にもわかりやすく解説します。

背景

Server Components の登場で変わったデータ取得

Next.js の App Router では、デフォルトでコンポーネントが Server Components として動作します。これにより、サーバー側で直接データベースにアクセスしたり、API を呼び出したりできるようになりました。

従来の Pages Router では、ページレベルでのみデータ取得が可能でしたが、Server Components ではコンポーネント単位でのデータ取得が実現できます。この変化により、よりきめ細かいデータ管理とキャッシュ戦略が必要になってきました。

以下の図は、Pages Router と App Router におけるデータ取得の違いを示しています。

mermaidflowchart TB
  subgraph old["Pages Router(従来)"]
    page1["ページ"] -->|getServerSideProps| data1["データ取得"]
    data1 --> comp1["コンポーネント A"]
    data1 --> comp2["コンポーネント B"]
  end

  subgraph new["App Router(新しい)"]
    page2["ページ"] --> sc1["Server Component A<br/>(直接データ取得)"]
    page2 --> sc2["Server Component B<br/>(直接データ取得)"]
    sc1 -->|fetch| api1["API/DB"]
    sc2 -->|fetch| api2["API/DB"]
  end

図で理解できる要点:

  • Pages Router ではページ単位でのデータ取得が必須でした
  • App Router では各コンポーネントが独立してデータを取得できます
  • より柔軟で細かいデータ管理が可能になりました

fetch の拡張機能

Next.js 13 以降では、標準の fetch API が拡張され、Next.js 独自のキャッシュ機能が組み込まれています。

これにより、以下のような恩恵を受けられます。

#機能説明
1自動キャッシュデフォルトでリクエストがキャッシュされる
2リクエスト重複排除同じリクエストは自動的に 1 回にまとめられる
3柔軟な再検証時間ベースやオンデマンドでキャッシュを更新できる
4セグメント単位の制御ルートセグメントごとにキャッシュ戦略を設定可能

課題

従来のデータ取得における問題点

Pages Router の時代には、いくつかの課題がありました。

まず、ページレベルでしかデータ取得ができないため、複数のコンポーネントで異なるデータが必要な場合、すべてを一度に取得する必要がありました。これにより、不要なデータまで取得してしまい、パフォーマンスに影響を与えることがありました。

また、getStaticProps でキャッシュする場合は、再検証のタイミングが限定的で、リアルタイム性が求められるデータには不向きでした。一方、getServerSideProps を使うとリクエストごとにデータを取得するため、サーバー負荷が高くなります。

キャッシュ戦略の複雑さ

Server Components の登場により柔軟なデータ取得が可能になった反面、適切なキャッシュ戦略を選ばないとパフォーマンス問題や古いデータの表示といった新たな課題が生まれます。

以下の図は、キャッシュ戦略の選択が難しい理由を示しています。

mermaidflowchart TD
  start["データ取得の要件"] --> q1{データの<br/>更新頻度は?}
  q1 -->|高頻度| realtime["リアルタイム性<br/>が必要"]
  q1 -->|低頻度| static_data["静的データ<br/>で十分"]

  realtime --> q2{キャッシュ<br/>は必要?}
  q2 -->|必要| short_cache["短時間キャッシュ"]
  q2 -->|不要| no_cache["キャッシュなし"]

  static_data --> q3{更新タイミング<br/>は?}
  q3 -->|定期的| revalidate["時間ベース再検証"]
  q3 -->|不定期| on_demand["オンデマンド再検証"]

  short_cache --> problem1["パフォーマンスと<br/>新鮮さのバランス"]
  no_cache --> problem2["サーバー負荷増大"]
  revalidate --> problem3["最適な間隔の決定"]
  on_demand --> problem4["再検証のタイミング<br/>管理"]

図で理解できる要点:

  • データの性質によって最適なキャッシュ戦略が異なります
  • それぞれの戦略には固有の課題があります
  • パフォーマンスと新鮮さのバランスが重要です

よくある誤解と落とし穴

Next.js の新しいキャッシュシステムについて、以下のような誤解がよくあります。

#誤解実際
1fetch は常にキャッシュされる動的レンダリングでは無効化されることがある
2キャッシュは永続的ビルド時や再検証時にクリアされる
3すべてのリクエストが重複排除される同一レンダリング内でのみ有効
4revalidate を設定すれば常に新しい次のリクエストまで古いデータが返る場合がある

これらの誤解は、予期しない動作やパフォーマンス問題を引き起こす原因となります。

解決策

Next.js fetch のキャッシュオプション

Next.js の拡張 fetch には、3 つの主要なキャッシュ制御オプションがあります。それぞれを正しく理解し、使い分けることが重要です。

cache オプション

cache オプションは、リクエストのキャッシュ動作を制御します。

typescript// force-cache: デフォルト動作、キャッシュを利用
fetch('https://api.example.com/data', {
  cache: 'force-cache',
});

上記コードは、データを強制的にキャッシュします。一度取得したデータは、再検証されるまで再利用されます。

typescript// no-store: キャッシュを使用せず、毎回新しいデータを取得
fetch('https://api.example.com/realtime', {
  cache: 'no-store',
});

こちらは、リアルタイム性が重要なデータに適しています。毎回サーバーにリクエストが送られるため、常に最新のデータが取得できます。

next.revalidate オプション

next.revalidate オプションでは、時間ベースの再検証を設定します。

typescript// 60秒ごとにキャッシュを再検証
fetch('https://api.example.com/news', {
  next: { revalidate: 60 },
});

このコードでは、60 秒経過後の次のリクエストでデータが再取得されます。ISR(Incremental Static Regeneration)と同様の動作です。

typescript// 1時間ごとに再検証
fetch('https://api.example.com/articles', {
  next: { revalidate: 3600 },
});

更新頻度が低いデータには、長めの再検証間隔を設定することでサーバー負荷を軽減できます。

next.tags オプション

next.tags オプションは、オンデマンド再検証のためのタグを設定します。

typescript// タグ付きでデータを取得
fetch('https://api.example.com/posts/123', {
  next: { tags: ['post', 'post-123'] },
});

タグを設定しておくことで、後から特定のデータのみを再検証できます。

キャッシュ戦略の使い分け

データの性質に応じて、適切なキャッシュ戦略を選択する必要があります。以下の表を参考にしてください。

#データの種類推奨戦略設定例
1静的コンテンツ(会社情報など)force-cache(デフォルト)cache: 'force-cache'
2定期更新コンテンツ(ブログ記事)時間ベース再検証next: { revalidate: 3600 }
3ユーザー依存データ(マイページ)no-storecache: 'no-store'
4CMS コンテンツタグベース再検証next: { tags: ['cms'] }
5リアルタイムデータ(株価など)no-storecache: 'no-store'

オンデマンド再検証の実装

オンデマンド再検証は、必要なタイミングでキャッシュを更新する強力な機能です。

以下の図は、オンデマンド再検証の流れを示しています。

mermaidsequenceDiagram
  participant Admin as 管理者
  participant API as API Route
  participant Next as Next.js
  participant Cache as キャッシュ
  participant User as ユーザー

  Admin->>API: コンテンツ更新
  API->>Next: revalidateTag('post')
  Next->>Cache: タグに紐づく<br/>キャッシュを無効化
  Cache-->>Next: 無効化完了
  Next-->>API: 成功レスポンス
  API-->>Admin: 更新完了

  User->>Next: ページアクセス
  Next->>Cache: キャッシュ確認
  Cache-->>Next: キャッシュなし
  Next->>Next: 新しいデータ取得
  Next->>Cache: 新しいキャッシュ保存
  Next-->>User: 最新ページ表示

図で理解できる要点:

  • 管理者がコンテンツを更新すると、即座にキャッシュが無効化されます
  • ユーザーの次のアクセス時に新しいデータが取得されます
  • CMS 連携など、コンテンツ更新が不定期な場合に最適です

パスベースの再検証

特定のパスのキャッシュを再検証する方法です。

typescript// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

まず、必要なモジュールをインポートします。revalidatePath は特定のパスのキャッシュを再検証する関数です。

typescriptexport async function POST(request: NextRequest) {
  // 認証トークンの確認
  const token = request.headers.get('authorization')

  if (token !== process.env.REVALIDATE_TOKEN) {
    return Response.json(
      { message: 'Invalid token' },
      { status: 401 }
    )
  }

セキュリティのため、環境変数で設定したトークンを確認します。これにより、認証されたリクエストのみがキャッシュを再検証できます。

typescript// パスを取得
const path = request.nextUrl.searchParams.get('path');

if (!path) {
  return Response.json(
    { message: 'Path is required' },
    { status: 400 }
  );
}

再検証するパスをクエリパラメータから取得します。

typescript  // パスのキャッシュを再検証
  revalidatePath(path)

  return Response.json({
    revalidated: true,
    now: Date.now(),
    path: path
  })
}

revalidatePath を実行して、指定されたパスのキャッシュを無効化します。次回のアクセス時に新しいデータが取得されます。

タグベースの再検証

より柔軟な再検証が可能なタグベースの方法です。

typescript// app/api/revalidate-tag/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

revalidateTag を使用すると、タグに紐づくすべてのキャッシュを一括で再検証できます。

typescriptexport async function POST(request: NextRequest) {
  const token = request.headers.get('authorization')

  if (token !== process.env.REVALIDATE_TOKEN) {
    return Response.json(
      { message: 'Invalid token' },
      { status: 401 }
    )
  }

パスベースと同様に、認証チェックを行います。

typescript// タグを取得
const tag = request.nextUrl.searchParams.get('tag');

if (!tag) {
  return Response.json(
    { message: 'Tag is required' },
    { status: 400 }
  );
}

再検証するタグをクエリパラメータから取得します。

typescript  // タグに紐づくすべてのキャッシュを再検証
  revalidateTag(tag)

  return Response.json({
    revalidated: true,
    now: Date.now(),
    tag: tag
  })
}

revalidateTag を実行すると、指定されたタグが付いたすべての fetch リクエストのキャッシュが無効化されます。複数のページやコンポーネントで同じデータを使用している場合に便利です。

ルートセグメント設定

ルートセグメント単位でキャッシュ動作を制御することもできます。

typescript// app/dashboard/layout.tsx または page.tsx

// すべての fetch をキャッシュしない
export const dynamic = 'force-dynamic';

dynamic オプションを force-dynamic に設定すると、そのルートセグメント内のすべての fetch が cache: 'no-store' と同じ動作になります。

typescript// デフォルトの再検証時間を設定
export const revalidate = 3600; // 1時間

ルートセグメント全体に対して、デフォルトの再検証時間を設定できます。個別の fetch で設定を上書きすることも可能です。

typescript// ランタイムをEdgeに設定
export const runtime = 'edge';

Edge Runtime を使用すると、世界中のエッジロケーションでコードが実行され、レイテンシが低減されます。

これらの設定により、ページやレイアウト単位でキャッシュ戦略を統一できます。

具体例

ブログサイトでの実装例

実際のブログサイトを想定した実装例を見ていきましょう。

記事一覧ページ

記事一覧は定期的に更新されるため、時間ベースの再検証が適しています。

typescript// app/blog/page.tsx
import Link from 'next/link';

// 型定義
interface Post {
  id: number;
  title: string;
  excerpt: string;
  publishedAt: string;
}

まず、記事データの型を定義します。TypeScript を使うことで、型安全なコードが書けます。

typescript// 記事一覧を取得する関数
async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://api.example.com/posts', {
    next: {
      revalidate: 600, // 10分ごとに再検証
      tags: ['posts'], // タグも設定
    },
  });

  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }

  return res.json();
}

10 分ごとに再検証することで、パフォーマンスと新鮮さのバランスを取っています。タグも設定しておくことで、必要に応じてオンデマンド再検証も可能です。

typescript// Server Component
export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>ブログ記事一覧</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link href={`/blog/${post.id}`}>
              <h2>{post.title}</h2>
              <p>{post.excerpt}</p>
              <time>{post.publishedAt}</time>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

Server Component では、トップレベルで await を使ってデータを取得できます。取得したデータはそのままレンダリングに使用できます。

記事詳細ページ

記事詳細ページでは、より細かい制御が必要です。

typescript// app/blog/[id]/page.tsx

// 型定義
interface Post {
  id: number;
  title: string;
  content: string;
  author: {
    name: string;
    avatar: string;
  };
  publishedAt: string;
}

interface Comment {
  id: number;
  author: string;
  content: string;
  createdAt: string;
}

記事とコメントの型を定義します。

typescript// 記事本体を取得(キャッシュあり)
async function getPost(id: string): Promise<Post> {
  const res = await fetch(
    `https://api.example.com/posts/${id}`,
    {
      next: {
        revalidate: 3600, // 1時間ごとに再検証
        tags: ['post', `post-${id}`], // 複数のタグを設定
      },
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch post');
  }

  return res.json();
}

記事本体は頻繁に更新されないため、1 時間の再検証間隔を設定しています。タグは postpost-{id} の 2 つを設定することで、全記事または特定の記事のみを再検証できます。

typescript// コメントを取得(キャッシュなし)
async function getComments(
  postId: string
): Promise<Comment[]> {
  const res = await fetch(
    `https://api.example.com/posts/${postId}/comments`,
    {
      cache: 'no-store', // 常に最新のコメントを表示
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch comments');
  }

  return res.json();
}

コメントは頻繁に追加されるため、キャッシュを使用せず常に最新のデータを取得します。

typescript// ページコンポーネント
export default async function PostPage({
  params
}: {
  params: { id: string }
}) {
  // 記事とコメントを並行取得
  const [post, comments] = await Promise.all([
    getPost(params.id),
    getComments(params.id)
  ])

Promise.all を使うことで、記事とコメントを並行して取得できます。これにより、取得時間を短縮できます。

typescript  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <div>
          <img src={post.author.avatar} alt={post.author.name} />
          <span>{post.author.name}</span>
          <time>{post.publishedAt}</time>
        </div>
      </header>

      <div dangerouslySetInnerHTML={{ __html: post.content }} />

記事本体をレンダリングします。HTML コンテンツの場合は dangerouslySetInnerHTML を使用しますが、XSS 対策のため、サーバー側でサニタイズされたコンテンツであることを確認してください。

typescript      <section>
        <h2>コメント ({comments.length}件)</h2>
        <ul>
          {comments.map(comment => (
            <li key={comment.id}>
              <strong>{comment.author}</strong>
              <p>{comment.content}</p>
              <time>{comment.createdAt}</time>
            </li>
          ))}
        </ul>
      </section>
    </article>
  )
}

コメント一覧を表示します。コメントは常に最新のデータが表示されるため、ユーザーがコメントを投稿した直後でも正しく表示されます。

EC サイトでの実装例

EC サイトでは、商品データと在庫データで異なるキャッシュ戦略が必要です。

以下の図は、EC サイトにおけるデータ取得とキャッシュ戦略の全体像を示しています。

mermaidflowchart TB
  subgraph page["商品詳細ページ"]
    sc1["Server Component<br/>(商品情報)"]
    sc2["Server Component<br/>(在庫情報)"]
    sc3["Server Component<br/>(レビュー)"]
  end

  sc1 -->|"revalidate: 3600<br/>tags: ['product']"| api1["商品 API<br/>(1時間キャッシュ)"]
  sc2 -->|"cache: 'no-store'"| api2["在庫 API<br/>(キャッシュなし)"]
  sc3 -->|"revalidate: 600<br/>tags: ['review']"| api3["レビュー API<br/>(10分キャッシュ)"]

  admin["管理画面"] -->|商品更新| webhook["Webhook"]
  webhook -->|revalidateTag| sc1
  webhook -->|revalidateTag| sc3

図で理解できる要点:

  • 商品情報は長めのキャッシュで効率化
  • 在庫情報はリアルタイム性を重視してキャッシュなし
  • レビューは短めのキャッシュでバランスを取る
  • 管理画面からの更新時はオンデマンド再検証

商品詳細ページ

typescript// app/products/[id]/page.tsx

// 型定義
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  images: string[];
  category: string;
}

interface Stock {
  available: boolean;
  quantity: number;
}

interface Review {
  id: number;
  author: string;
  rating: number;
  comment: string;
  createdAt: string;
}

商品、在庫、レビューの型を定義します。

typescript// 商品情報を取得(長めのキャッシュ)
async function getProduct(id: string): Promise<Product> {
  const res = await fetch(
    `https://api.example.com/products/${id}`,
    {
      next: {
        revalidate: 3600, // 1時間
        tags: ['product', `product-${id}`],
      },
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch product');
  }

  return res.json();
}

商品情報は頻繁に変更されないため、1 時間のキャッシュを設定します。

typescript// 在庫情報を取得(キャッシュなし)
async function getStock(productId: string): Promise<Stock> {
  const res = await fetch(
    `https://api.example.com/products/${productId}/stock`,
    {
      cache: 'no-store', // 常に最新の在庫を確認
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch stock');
  }

  return res.json();
}

在庫情報はリアルタイム性が重要なため、キャッシュを使用しません。これにより、売り切れ商品の購入を防げます。

typescript// レビューを取得(短めのキャッシュ)
async function getReviews(
  productId: string
): Promise<Review[]> {
  const res = await fetch(
    `https://api.example.com/products/${productId}/reviews`,
    {
      next: {
        revalidate: 600, // 10分
        tags: ['review', `review-${productId}`],
      },
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch reviews');
  }

  return res.json();
}

レビューは定期的に追加されるため、10 分の再検証間隔を設定します。

typescript// ページコンポーネント
export default async function ProductPage({
  params
}: {
  params: { id: string }
}) {
  // すべてのデータを並行取得
  const [product, stock, reviews] = await Promise.all([
    getProduct(params.id),
    getStock(params.id),
    getReviews(params.id)
  ])

3 つのデータソースを並行して取得することで、パフォーマンスを最適化します。

typescript  return (
    <div>
      <div>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
        <p>価格: ¥{product.price.toLocaleString()}</p>

        {/* 在庫状況 */}
        {stock.available ? (
          <div>
            <span>在庫あり</span>
            <span>残り{stock.quantity}点</span>
            <button>カートに追加</button>
          </div>
        ) : (
          <div>
            <span>売り切れ</span>
          </div>
        )}
      </div>

在庫情報に基づいて、カートに追加ボタンの表示を切り替えます。

typescript      {/* レビュー */}
      <section>
        <h2>カスタマーレビュー</h2>
        <div>
          {reviews.map(review => (
            <div key={review.id}>
              <strong>{review.author}</strong>
              <span>★{review.rating}</span>
              <p>{review.comment}</p>
              <time>{review.createdAt}</time>
            </div>
          ))}
        </div>
      </section>
    </div>
  )
}

レビュー一覧を表示します。10 分ごとに更新されるため、新しいレビューも適度なタイミングで反映されます。

CMS 連携での実装例

CMS からコンテンツを取得する場合、Webhook と組み合わせたオンデマンド再検証が効果的です。

CMS データ取得

typescript// lib/cms.ts

// 型定義
interface Article {
  id: string;
  title: string;
  content: string;
  slug: string;
  publishedAt: string;
  updatedAt: string;
}

CMS の記事データの型を定義します。

typescript// CMSから記事一覧を取得
export async function getArticles(): Promise<Article[]> {
  const res = await fetch(
    'https://cms.example.com/api/articles',
    {
      headers: {
        Authorization: `Bearer ${process.env.CMS_API_KEY}`,
      },
      next: {
        tags: ['articles'],
      },
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch articles');
  }

  return res.json();
}

CMS API にアクセスする際は、認証ヘッダーを付与します。環境変数で API キーを管理することで、セキュリティを確保します。

typescript// 個別記事を取得
export async function getArticle(
  slug: string
): Promise<Article> {
  const res = await fetch(
    `https://cms.example.com/api/articles/${slug}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.CMS_API_KEY}`,
      },
      next: {
        tags: ['article', `article-${slug}`],
      },
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch article');
  }

  return res.json();
}

記事ごとに個別のタグを設定することで、特定の記事のみを再検証できます。

Webhook エンドポイント

CMS からの Webhook を受け取って、キャッシュを再検証します。

typescript// app/api/webhook/cms/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

必要なモジュールをインポートします。

typescript// Webhook署名の検証
function verifyWebhookSignature(
  payload: string,
  signature: string
): boolean {
  // 実際の実装では、CMSプロバイダーが提供する
  // 署名検証ロジックを使用してください
  const expectedSignature = // ... 署名を計算

  return signature === expectedSignature
}

Webhook のセキュリティを確保するため、署名を検証します。これにより、正規の CMS からのリクエストのみを受け付けます。

typescriptexport async function POST(request: NextRequest) {
  try {
    // リクエストボディを取得
    const payload = await request.text()
    const signature = request.headers.get('x-webhook-signature')

    // 署名を検証
    if (!signature || !verifyWebhookSignature(payload, signature)) {
      return Response.json(
        { error: 'Invalid signature' },
        { status: 401 }
      )
    }

署名が無効な場合は、401 エラーを返します。

typescript// ペイロードをパース
const data = JSON.parse(payload);
const { event, article } = data;

Webhook のペイロードから、イベントタイプと記事情報を取得します。

typescript// イベントに応じて再検証
switch (event) {
  case 'article.created':
  case 'article.updated':
    // 個別記事と一覧を再検証
    revalidateTag(`article-${article.slug}`);
    revalidateTag('articles');
    break;

  case 'article.deleted':
    // 一覧のみ再検証
    revalidateTag('articles');
    break;
}

イベントタイプに応じて、適切なタグを再検証します。記事の作成や更新時は、その記事と一覧の両方を再検証します。

typescript    return Response.json({
      revalidated: true,
      now: Date.now()
    })
  } catch (error) {
    console.error('Webhook error:', error)
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

エラーが発生した場合は、ログに記録して 500 エラーを返します。

記事ページ

typescript// app/articles/[slug]/page.tsx
import { getArticle } from '@/lib/cms';

export default async function ArticlePage({
  params,
}: {
  params: { slug: string };
}) {
  const article = await getArticle(params.slug);

  return (
    <article>
      <h1>{article.title}</h1>
      <time>公開日: {article.publishedAt}</time>
      <time>更新日: {article.updatedAt}</time>
      <div
        dangerouslySetInnerHTML={{
          __html: article.content,
        }}
      />
    </article>
  );
}

CMS からデータを取得して表示します。CMS で記事を更新すると、Webhook 経由で自動的にキャッシュが再検証され、最新の内容が表示されます。

パフォーマンス最適化のポイント

実装時に意識すべきパフォーマンス最適化のポイントを紹介します。

#ポイント説明効果
1並行データ取得Promise.all で複数の fetch を同時実行取得時間の短縮
2適切なキャッシュ戦略データの性質に応じた cache オプション選択サーバー負荷軽減
3タグの活用関連するデータに同じタグを設定効率的な再検証
4ストリーミングSuspense を使った段階的レンダリング初期表示の高速化
5エラーハンドリングError Boundary での適切なエラー処理ユーザー体験の向上

これらのポイントを意識することで、高速で信頼性の高いアプリケーションを構築できます。

まとめ

Next.js の App Router と Server Components により、データ取得とキャッシュ戦略が大きく進化しました。本記事で解説した内容をまとめます。

キャッシュオプションの使い分けが最も重要です。cache: 'force-cache' はデフォルトで静的コンテンツに、cache: 'no-store' はリアルタイム性が必要なデータに使用します。next.revalidate では時間ベースの再検証を設定でき、定期的に更新されるコンテンツに最適です。

オンデマンド再検証は、CMS 連携や管理画面からのコンテンツ更新時に威力を発揮します。revalidatePath でパス単位、revalidateTag でタグ単位の再検証が可能で、Webhook と組み合わせることで自動的なキャッシュ更新が実現できます。

データの性質に応じた戦略選択が成功の鍵となります。商品情報は長めのキャッシュ、在庫情報はキャッシュなし、レビューは短めのキャッシュというように、各データの特性を理解して適切な設定を行うことで、パフォーマンスとユーザー体験の両立が可能になります。

Server Components の登場により、より柔軟で効率的なデータ取得が可能になりました。本記事で紹介したパターンを参考に、プロジェクトに最適なキャッシュ戦略を構築してください。最初は試行錯誤が必要かもしれませんが、データの性質を見極めて適切な設定を行うことで、高速で快適なアプリケーションを実現できるでしょう。

関連リンク