T-CREATOR

Next.js で「Dynamic server usage: cookies/headers」はなぜ起きる?原因と解決手順

Next.js で「Dynamic server usage: cookies/headers」はなぜ起きる?原因と解決手順

Next.js 14 の App Router を使用していて、突然「Dynamic server usage: cookies」や「Dynamic server usage: headers」というエラーに遭遇したことはありませんか?このエラーは、静的生成(SSG)を期待していたページが動的レンダリング(SSR)に強制変更される際に発生します。

本記事では、このエラーが発生する根本的な原因から具体的な解決手順まで、実務で直面する問題を段階的に解決していきます。パフォーマンスを維持しながらエラーを解消する方法をマスターしましょう。

背景

Next.js の静的生成とサーバーサイドレンダリング

Next.js は、パフォーマンスを最大化するために複数のレンダリング戦略を提供しています。特に App Router では、デフォルトで静的生成(SSG)を選択し、必要に応じて動的レンダリング(SSR)に切り替わります。

以下の図で、レンダリング戦略の選択プロセスを確認してみましょう。

mermaidflowchart TD
    start[ページアクセス] --> check{Dynamic API使用?}
    check -->|No| ssg[Static Generation<br/>ビルド時生成]
    check -->|Yes| ssr[Dynamic Rendering<br/>リクエスト時生成]
    ssg --> cache[CDNキャッシュ<br/>高速配信]
    ssr --> process[サーバー処理<br/>リアルタイム生成]
    cache --> user[ユーザーへ配信]
    process --> user

静的生成では、ビルド時に HTML が生成され、CDN でキャッシュされて高速に配信されます。一方、動的レンダリングでは、リクエストごとにサーバーで HTML が生成されるため、レスポンス時間が長くなる可能性があります。

App Router でのデータ取得の変更点

App Router では、従来の Pages Router とは大きく異なるデータ取得パターンが導入されました。最も重要な変更点は、Server Components でのデータ取得方法です。

従来の Pages Router でのデータ取得:

typescript// pages/profile.tsx (Pages Router)
export async function getStaticProps() {
  // ビルド時のデータ取得
  return {
    props: { data: 'static data' },
  };
}

export default function Profile({ data }) {
  return <div>{data}</div>;
}

App Router での新しいアプローチ:

typescript// app/profile/page.tsx (App Router)
export default async function Profile() {
  // Server Component内で直接データ取得
  const data = await fetch('https://api.example.com/data');
  return <div>{data}</div>;
}

この変更により、Server Component 内でcookies()headers()を使用すると、自動的に動的レンダリングが選択されるようになりました。

cookies() と headers() 関数の役割

App Router では、next​/​headersから提供されるcookies()headers()関数を使用して、リクエスト情報にアクセスできます。

typescriptimport { cookies, headers } from 'next/headers';

export default async function ServerComponent() {
  // Cookieの取得
  const cookieStore = cookies();
  const token = cookieStore.get('auth-token');

  // ヘッダーの取得
  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  return <div>認証トークン: {token?.value}</div>;
}

これらの関数は、リクエスト固有の情報を取得するため、使用すると自動的にページが動的レンダリングに変更されます。

課題

エラーが発生する具体的な条件

「Dynamic server usage」エラーは、以下の条件で発生します。主な発生パターンを整理してみましょう。

#発生条件エラーメッセージ影響範囲
1Server Component でcookies()使用Dynamic server usage: cookiesページ全体
2Server Component でheaders()使用Dynamic server usage: headersページ全体
3generateStaticParams と併用Static generation failedビルド時エラー
4export const dynamic = 'force-static'との競合Configuration conflictビルド時エラー

以下は、エラーが発生する典型的なコード例です:

typescript// app/dashboard/page.tsx
import { cookies } from 'next/headers';

// 静的生成を強制する設定
export const dynamic = 'force-static';

export default async function Dashboard() {
  // この時点でエラーが発生
  const cookieStore = cookies();
  const userId = cookieStore.get('user-id');

  return <div>ユーザーID: {userId?.value}</div>;
}

このコードでは、force-staticで静的生成を指定しているにも関わらず、cookies()を使用しているため矛盾が発生します。

Static Generation から Dynamic Rendering への強制変更

Next.js は、Dynamic API の使用を検出すると、自動的にページのレンダリング戦略を変更します。この変更プロセスを図で確認しましょう。

mermaidsequenceDiagram
    participant Build as ビルドプロセス
    participant Page as ページ解析
    participant Render as レンダリング決定
    participant Output as 出力生成

    Build->>Page: ページコード解析
    Page->>Page: cookies()/headers()検出
    Page->>Render: Dynamic API使用報告
    Render->>Render: SSG→SSR変更決定
    Render->>Output: 動的ページとして出力

    Note over Build,Output: Warning: Dynamic server usage detected

この自動変更により、開発者が意図しない動的レンダリングが発生し、パフォーマンスの低下につながる可能性があります。

パフォーマンスへの影響と SEO の問題

静的生成から動的レンダリングへの変更は、以下のような具体的な影響をもたらします。

パフォーマンスへの影響:

typescript// 静的生成の場合
// レスポンス時間: 10-50ms (CDNから配信)
// サーバー負荷: ほぼ0

// 動的レンダリングの場合
// レスポンス時間: 100-500ms (サーバー処理含む)
// サーバー負荷: リクエストごとに処理

SEO への影響:

  • クローラーの待機時間増加
  • Core Web Vitals スコアの低下
  • インデックス速度の遅延

これらの問題を解決するには、適切な戦略選択と実装が必要です。

解決策

条件分岐による cookies/headers 使用の制御

最も効果的な解決策の一つは、Dynamic API の使用を条件付きで制御することです。以下の実装例をご確認ください。

typescript// app/profile/page.tsx
import { cookies } from 'next/headers';

interface ProfileProps {
  searchParams: { [key: string]: string | undefined };
}

export default async function Profile({
  searchParams,
}: ProfileProps) {
  // URLパラメータでDynamic API使用を制御
  const isDynamic = searchParams.auth === 'required';

  let userToken = null;
  if (isDynamic) {
    const cookieStore = cookies();
    userToken = cookieStore.get('auth-token');
  }

  return (
    <div>
      <h1>プロフィールページ</h1>
      {userToken ? (
        <p>認証済みユーザー: {userToken.value}</p>
      ) : (
        <p>ゲストユーザー</p>
      )}
    </div>
  );
}

この方法では、特定の条件でのみ Dynamic API を使用するため、大部分のリクエストで静的生成を維持できます。

generateStaticParams での静的生成維持

動的ルートでも静的生成を維持するには、generateStaticParamsを適切に活用します。

typescript// app/posts/[id]/page.tsx
import { cookies } from 'next/headers';

// 静的パスを事前生成
export async function generateStaticParams() {
  const posts = await fetch(
    'https://api.example.com/posts'
  ).then((res) => res.json());

  return posts.map((post: any) => ({
    id: post.id.toString(),
  }));
}

// メタデータも静的生成
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}) {
  const post = await fetch(
    `https://api.example.com/posts/${params.id}`
  ).then((res) => res.json());

  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function PostPage({
  params,
}: {
  params: { id: string };
}) {
  // 基本データは静的取得
  const post = await fetch(
    `https://api.example.com/posts/${params.id}`
  ).then((res) => res.json());

  // 認証が必要な場合のみDynamic API使用
  let isLiked = false;
  try {
    const cookieStore = cookies();
    const authToken = cookieStore.get('auth-token');
    if (authToken) {
      const likeResponse = await fetch(
        `https://api.example.com/posts/${params.id}/like`,
        {
          headers: {
            Authorization: `Bearer ${authToken.value}`,
          },
        }
      );
      isLiked = likeResponse.ok;
    }
  } catch (error) {
    // Cookie使用をtry-catchで包むことで、エラー時の静的生成を保持
    console.warn('Dynamic content loading failed:', error);
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <div>いいね状況: {isLiked ? '❤️' : '🤍'}</div>
    </article>
  );
}

middleware での事前処理による回避策

Middleware を活用することで、Server Component 内での Dynamic API 使用を回避できます。

typescript// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Cookieをヘッダーに転送
  const authToken = request.cookies.get('auth-token');
  if (authToken) {
    response.headers.set('x-auth-token', authToken.value);
  }

  // ユーザーエージェントも転送
  const userAgent = request.headers.get('user-agent') || '';
  response.headers.set('x-user-agent', userAgent);

  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
};

Server Component 側では、転送されたヘッダーを使用:

typescript// app/dashboard/page.tsx
import { headers } from 'next/headers';

export default async function Dashboard() {
  const headersList = headers();

  // Middlewareで転送された値を取得(Dynamic APIは使わない)
  const authToken = headersList.get('x-auth-token');
  const userAgent = headersList.get('x-user-agent');

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>認証状況: {authToken ? '認証済み' : '未認証'}</p>
      <p>ブラウザ: {userAgent}</p>
    </div>
  );
}

認証状態など、ユーザー固有の情報は Client Component で処理することで、Server Component の静的生成を維持できます。

Server Component(静的生成維持):

typescript// app/posts/page.tsx
export default async function PostsPage() {
  // 静的データは Server Component で取得
  const posts = await fetch(
    'https://api.example.com/posts',
    {
      next: { revalidate: 3600 }, // 1時間ごとに再検証
    }
  ).then((res) => res.json());

  return (
    <div>
      <h1>投稿一覧</h1>
      <PostsList posts={posts} />
      <AuthStatus /> {/* Client Component */}
    </div>
  );
}

Client Component(動的コンテンツ):

typescript// components/AuthStatus.tsx
'use client';

import { useEffect, useState } from 'react';
import { getCookie } from 'cookies-next';

export default function AuthStatus() {
  const [authToken, setAuthToken] = useState<string | null>(
    null
  );
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // クライアントサイドでCookie取得
    const token = getCookie('auth-token');
    setAuthToken((token as string) || null);
    setLoading(false);
  }, []);

  if (loading) {
    return <div>認証状況確認中...</div>;
  }

  return (
    <div>
      認証状況:{' '}
      {authToken ? '✅ ログイン済み' : '❌ 未ログイン'}
    </div>
  );
}

この方法では、基本的なページ構造は静的生成され、ユーザー固有の情報のみクライアントサイドで動的に取得されます。

具体例

エラーが発生するコード例と修正版

実際のプロジェクトでよく遭遇するエラーパターンと、その修正版を段階的に説明します。

エラー発生コード:

typescript// app/shop/[category]/page.tsx - 問題のあるコード
import { cookies, headers } from 'next/headers';

export async function generateStaticParams() {
  return [
    { category: 'electronics' },
    { category: 'clothing' },
    { category: 'books' },
  ];
}

export default async function CategoryPage({
  params,
}: {
  params: { category: string };
}) {
  // これが原因でSSG→SSRに変更される
  const cookieStore = cookies();
  const userPreference = cookieStore.get('display-mode');

  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  // 商品データの取得
  const products = await fetch(
    `https://api.shop.com/categories/${params.category}`
  ).then((res) => res.json());

  return (
    <div>
      <h1>{params.category}の商品一覧</h1>
      <p>
        表示モード: {userPreference?.value || 'default'}
      </p>
      <p>ブラウザ: {userAgent}</p>
      {/* 商品リスト */}
    </div>
  );
}

このコードは以下のエラーを発生させます:

javascriptError: Dynamic server usage: cookies. This route cannot be prerendered because it reads `cookies()`.
Error: Dynamic server usage: headers. This route cannot be prerendered because it reads `headers()`.

修正版 1: 条件付き Dynamic API 使用

typescript// app/shop/[category]/page.tsx - 修正版1
import { cookies, headers } from 'next/headers';

export async function generateStaticParams() {
  return [
    { category: 'electronics' },
    { category: 'clothing' },
    { category: 'books' },
  ];
}

interface CategoryPageProps {
  params: { category: string };
  searchParams: { personalized?: string };
}

export default async function CategoryPage({
  params,
  searchParams,
}: CategoryPageProps) {
  // パーソナライズが要求された場合のみDynamic API使用
  const enablePersonalization =
    searchParams.personalized === 'true';

  let userPreference = null;
  let userAgent = null;

  if (enablePersonalization) {
    const cookieStore = cookies();
    userPreference = cookieStore.get('display-mode');

    const headersList = headers();
    userAgent = headersList.get('user-agent');
  }

  // 商品データは常に取得(静的生成)
  const products = await fetch(
    `https://api.shop.com/categories/${params.category}`,
    {
      next: { revalidate: 300 }, // 5分ごとに再検証
    }
  ).then((res) => res.json());

  return (
    <div>
      <h1>{params.category}の商品一覧</h1>
      {enablePersonalization && (
        <div>
          <p>
            表示モード: {userPreference?.value || 'default'}
          </p>
          <p>ブラウザ: {userAgent}</p>
        </div>
      )}
      <ProductGrid products={products} />
    </div>
  );
}

修正版 2: Hybrid アプローチ(推奨)

typescript// app/shop/[category]/page.tsx - 修正版2(推奨)
import { ProductGrid } from '@/components/ProductGrid';
import { PersonalizationPanel } from '@/components/PersonalizationPanel';

export async function generateStaticParams() {
  return [
    { category: 'electronics' },
    { category: 'clothing' },
    { category: 'books' },
  ];
}

// Server Component(静的生成)
export default async function CategoryPage({
  params,
}: {
  params: { category: string };
}) {
  // 基本データは静的に取得
  const [products, categoryInfo] = await Promise.all([
    fetch(
      `https://api.shop.com/categories/${params.category}`,
      {
        next: { revalidate: 300 },
      }
    ).then((res) => res.json()),
    fetch(
      `https://api.shop.com/categories/${params.category}/info`,
      {
        next: { revalidate: 3600 },
      }
    ).then((res) => res.json()),
  ]);

  return (
    <div>
      <h1>{categoryInfo.name}</h1>
      <p>{categoryInfo.description}</p>

      {/* Client Componentでパーソナライズ */}
      <PersonalizationPanel />

      {/* 静的に生成された商品リスト */}
      <ProductGrid products={products} />
    </div>
  );
}

Client Component 側:

typescript// components/PersonalizationPanel.tsx
'use client';

import { useEffect, useState } from 'react';
import { getCookie } from 'cookies-next';

export function PersonalizationPanel() {
  const [preferences, setPreferences] = useState({
    displayMode: 'grid',
    sortOrder: 'name',
  });
  const [userAgent, setUserAgent] = useState('');

  useEffect(() => {
    // クライアントサイドでCookie取得
    const displayMode =
      (getCookie('display-mode') as string) || 'grid';
    const sortOrder =
      (getCookie('sort-order') as string) || 'name';

    setPreferences({ displayMode, sortOrder });
    setUserAgent(navigator.userAgent);
  }, []);

  return (
    <div className='personalization-panel'>
      <h3>表示設定</h3>
      <p>表示モード: {preferences.displayMode}</p>
      <p>並び順: {preferences.sortOrder}</p>
      <details>
        <summary>ブラウザ情報</summary>
        <p>{userAgent}</p>
      </details>
    </div>
  );
}

実際のプロジェクトでの解決事例

ある E コマースサイトでの実際の改善事例を紹介します。

改善前の問題:

  • 商品一覧ページで認証状態を Server Component で確認
  • 全ページが Dynamic Rendering に変更
  • ページ読み込み時間が平均 400ms → 1200ms に悪化

改善後の構成:

typescript// app/products/page.tsx - 改善後
import { Suspense } from 'react';
import { ProductList } from '@/components/ProductList';
import { UserWishlist } from '@/components/UserWishlist';
import { RecentlyViewed } from '@/components/RecentlyViewed';

export const dynamic = 'force-static'; // 静的生成を強制

export default async function ProductsPage() {
  // 基本的な商品データは静的に取得
  const featuredProducts = await fetch(
    'https://api.shop.com/products/featured',
    {
      next: { revalidate: 600 }, // 10分ごとに更新
    }
  ).then((res) => res.json());

  return (
    <div>
      <h1>商品一覧</h1>

      {/* 静的コンテンツ */}
      <ProductList products={featuredProducts} />

      {/* 動的コンテンツ(Client Components) */}
      <Suspense
        fallback={<div>ウィッシュリスト読み込み中...</div>}
      >
        <UserWishlist />
      </Suspense>

      <Suspense fallback={<div>閲覧履歴読み込み中...</div>}>
        <RecentlyViewed />
      </Suspense>
    </div>
  );
}

ユーザー固有のデータ取得:

typescript// components/UserWishlist.tsx
'use client';

import { useEffect, useState } from 'react';
import { getCookie } from 'cookies-next';

export function UserWishlist() {
  const [wishlistItems, setWishlistItems] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadWishlist() {
      const authToken = getCookie('auth-token');
      if (!authToken) {
        setLoading(false);
        return;
      }

      try {
        const response = await fetch('/api/wishlist', {
          headers: {
            Authorization: `Bearer ${authToken}`,
          },
        });

        if (response.ok) {
          const items = await response.json();
          setWishlistItems(items);
        }
      } catch (error) {
        console.error('ウィッシュリスト取得エラー:', error);
      } finally {
        setLoading(false);
      }
    }

    loadWishlist();
  }, []);

  if (loading) return <div>読み込み中...</div>;
  if (wishlistItems.length === 0)
    return <div>ウィッシュリストは空です</div>;

  return (
    <div>
      <h3>ウィッシュリスト</h3>
      {/* ウィッシュリストアイテム表示 */}
    </div>
  );
}

パフォーマンス改善の測定結果

上記の改善により、以下のような具体的なパフォーマンス向上が実現されました。

指標改善前改善後改善率
TTFB(Time to First Byte)800ms120ms85%改善
FCP(First Contentful Paint)1200ms300ms75%改善
LCP(Largest Contentful Paint)2100ms800ms62%改善
サーバー負荷100%15%85%削減
CDN キャッシュヒット率20%95%75 ポイント向上

これらの改善は、以下の戦略により実現されました:

  1. 静的生成の最大化: 基本コンテンツの静的生成維持
  2. 段階的ハイドレーション: 必要な部分のみクライアントサイドで動的取得
  3. 効率的なキャッシュ戦略: ISR による適切な再検証間隔設定

パフォーマンス測定には、以下のツールを活用しました:

typescript// tools/performance-monitor.ts
export async function measurePagePerformance(url: string) {
  const startTime = performance.now();

  const response = await fetch(url);
  const ttfb = performance.now() - startTime;

  const html = await response.text();
  const endTime = performance.now();

  return {
    ttfb,
    totalTime: endTime - startTime,
    cacheStatus:
      response.headers.get('x-vercel-cache') || 'UNKNOWN',
    size: html.length,
  };
}

// 使用例
const metrics = await measurePagePerformance(
  'https://shop.example.com/products'
);
console.log('Performance Metrics:', metrics);

図で改善の要点をまとめると:

  • 基本戦略: Static Generation を最大限活用
  • 動的要素: Client Components で必要最小限の動的処理
  • キャッシュ最適化: CDN キャッシュヒット率の大幅向上

まとめ

Next.js の「Dynamic server usage: cookies/headers」エラーは、Static Generation から Dynamic Rendering への意図しない変更が原因で発生します。このエラーを解決するには、以下の戦略的アプローチが効果的です。

重要なポイント:

  1. 条件付き Dynamic API 使用 - 必要な場合のみcookies()headers()を使用
  2. Hybrid アプローチ - Server Component と Client Component を適切に分離
  3. Middleware の活用 - Cookie/Header 情報の事前処理による回避
  4. 段階的最適化 - パフォーマンス指標を測定しながら改善

適切な実装により、ユーザーエクスペリエンスを維持しながらパフォーマンスの大幅な改善が可能です。特に、基本コンテンツを静的生成し、ユーザー固有の情報のみクライアントサイドで取得する戦略は、多くのケースで効果的な解決策となります。

エラーが発生した際は、まず影響範囲を特定し、段階的に修正を適用することで、安全かつ効率的な解決が実現できるでしょう。

関連リンク