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」エラーは、以下の条件で発生します。主な発生パターンを整理してみましょう。
# | 発生条件 | エラーメッセージ | 影響範囲 |
---|---|---|---|
1 | Server Component でcookies() 使用 | Dynamic server usage: cookies | ページ全体 |
2 | Server Component でheaders() 使用 | Dynamic server usage: headers | ページ全体 |
3 | generateStaticParams と併用 | Static generation failed | ビルド時エラー |
4 | export 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>
);
}
クライアントサイドでの cookie 取得への変更
認証状態など、ユーザー固有の情報は 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) | 800ms | 120ms | 85%改善 |
FCP(First Contentful Paint) | 1200ms | 300ms | 75%改善 |
LCP(Largest Contentful Paint) | 2100ms | 800ms | 62%改善 |
サーバー負荷 | 100% | 15% | 85%削減 |
CDN キャッシュヒット率 | 20% | 95% | 75 ポイント向上 |
これらの改善は、以下の戦略により実現されました:
- 静的生成の最大化: 基本コンテンツの静的生成維持
- 段階的ハイドレーション: 必要な部分のみクライアントサイドで動的取得
- 効率的なキャッシュ戦略: 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 への意図しない変更が原因で発生します。このエラーを解決するには、以下の戦略的アプローチが効果的です。
重要なポイント:
- 条件付き Dynamic API 使用 - 必要な場合のみ
cookies()
やheaders()
を使用 - Hybrid アプローチ - Server Component と Client Component を適切に分離
- Middleware の活用 - Cookie/Header 情報の事前処理による回避
- 段階的最適化 - パフォーマンス指標を測定しながら改善
適切な実装により、ユーザーエクスペリエンスを維持しながらパフォーマンスの大幅な改善が可能です。特に、基本コンテンツを静的生成し、ユーザー固有の情報のみクライアントサイドで取得する戦略は、多くのケースで効果的な解決策となります。
エラーが発生した際は、まず影響範囲を特定し、段階的に修正を適用することで、安全かつ効率的な解決が実現できるでしょう。
関連リンク
- article
Next.js で「Dynamic server usage: cookies/headers」はなぜ起きる?原因と解決手順
- article
Zustand × Next.js の Hydration Mismatch を根絶する:原因別チェックリスト
- article
Next.js の Parallel Routes & Intercepting Routes を図解で理解する最新入門
- article
Convex × React/Next.js 最速連携:useQuery/useMutation の実践パターン
- article
Next.js の Middleware 活用法:リクエスト制御・認証・リダイレクトの実践例
- article
【解決策】Next.js での CORS エラーの原因と対処方法まとめ
- article
Astro × Cloudflare Workers/Pages:エッジ配信で超高速なサイトを構築
- article
【2025 年版】Playwright vs Cypress vs Selenium 徹底比較:速度・安定性・学習コストの最適解
- article
Apollo を最短導入:Vite/Next.js/Remix での初期配線テンプレ集
- article
Zod 導入最短ルート:yarn/pnpm/bun でのセットアップと型サポート
- article
Node.js × Corepack 時代のパッケージマネージャ運用:npm/pnpm/yarn を賢く切替
- article
Next.js で「Dynamic server usage: cookies/headers」はなぜ起きる?原因と解決手順
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来