Next.js 15 Partial Pre-rendering でEコマースサイトのSEOを改善する実践ガイド
Next.js 15の新機能Partial Pre-renderingを使ってEコマースサイトのSEO・パフォーマンスを両立する実装方法を解説。商品詳細ページの最適化からビルド時間短縮まで実践テクニック満載。
Next.js 15 Partial Pre-renderingとは何か
Next.js 15で導入されたPartial Pre-rendering(PPR)は、静的なコンテンツと動的なコンテンツを同一ページ内で混在させるレンダリング手法です。従来のSSG(Static Site Generation)とSSR(Server-Side Rendering)の二者択一ではなく、ページ内の一部を静的に、残りを動的にレンダリングできます。
従来のEコマースサイトでは、商品詳細ページ全体をSSRにするか、完全に静的化するかの選択を迫られていました。SSRにすると初期表示が遅くなり、静的化すると在庫情報やユーザー固有データが反映されません。PPRはこのジレンマを解決します。
PPRの仕組み:
- ページの静的部分(商品説明・画像など)をビルド時に生成
- 動的部分(在庫数・カート状態など)はリクエスト時にサーバーでレンダリング
- 静的部分を即座に表示し、動的部分はStreaming SSRで後から挿入
flowchart TD
A["リクエスト受信"] --> B["静的シェルを即座に返却"]
B --> C["ブラウザに初期HTML表示"]
B --> D["サーバーで動的部分を生成"]
D --> E["Streamingで動的コンテンツを挿入"]
C --> F["完全な描画完了"]
E --> F
上記フローで、ユーザーは静的コンテンツを待たずに閲覧開始でき、LCP(Largest Contentful Paint)が改善されます。
PPRは特にEコマースサイトで有効です。商品説明や画像は静的にキャッシュし、在庫数や価格は動的に取得することで、SEOとパフォーマンスを両立できます。
Eコマースサイトでの具体的な活用パターン
商品詳細ページの最適化
商品詳細ページは、Eコマースサイトで最も重要なSEO対象です。PPRを使えば、静的な商品情報(タイトル・説明・画像)をクローラーに即座に提供しつつ、動的なユーザー体験(在庫数・レビュー)を維持できます。
実装例(App Router):
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { ProductInfo } from '@/components/ProductInfo';
import { StockStatus } from '@/components/StockStatus';
import { Reviews } from '@/components/Reviews';
export const experimental_ppr = true; // PPRを有効化
export default async function ProductPage({ params }: { params: { id: string } }) {
// 静的部分: ビルド時に取得
const product = await fetchProductStatic(params.id);
return (
<div>
{/* 静的コンテンツ: 即座に表示 */}
<ProductInfo product={product} />
{/* 動的コンテンツ: Streaming SSRで後から挿入 */}
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
</div>
);
}
この実装のSEOメリット:
- Googlebotは静的部分(商品名・説明・画像)を即座にクロール可能
- 構造化データ(JSON-LD)を静的部分に含めればリッチリザルト対応
- TTFBが短縮され、Core Web Vitalsが向上
カテゴリページのパフォーマンス最適化
カテゴリページでは、商品一覧は静的に生成し、フィルター結果やソート順は動的に処理する設計が有効です。
// app/category/[slug]/page.tsx
export const experimental_ppr = true;
export default async function CategoryPage({ params }: { params: { slug: string } }) {
// 静的: 初期の商品一覧(デフォルト表示順)
const products = await fetchCategoryProducts(params.slug);
return (
<div>
<h1>{params.slug} カテゴリ</h1>
{/* 静的な初期表示 */}
<ProductGrid products={products} />
{/* 動的: フィルター結果 */}
<Suspense fallback={<FilterSkeleton />}>
<FilteredResults slug={params.slug} />
</Suspense>
</div>
);
}
ユーザー固有UIとSEOの両立
「カートに追加」ボタンや「お気に入り」アイコンなど、ユーザーログイン状態に依存するUIは動的にレンダリングし、商品情報は静的にすることでSEOクローラーには完全な情報を提供できます。
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProductStatic(params.id);
return (
<>
{/* 静的: SEO重要コンテンツ */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"image": product.imageUrl,
"description": product.description,
})}
</script>
<h1>{product.name}</h1>
<img src={product.imageUrl} alt={product.name} />
{/* 動的: ユーザー依存UI */}
<Suspense fallback={<ButtonSkeleton />}>
<AddToCartButton productId={params.id} />
</Suspense>
</>
);
}
構造化データとメタタグの最適配置
PPRを使う際、SEO重要要素(構造化データ・メタタグ)は必ず静的部分に配置する必要があります。動的部分に配置するとクローラーが認識できません。
構造化データの静的生成
// app/products/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const product = await fetchProductStatic(params.id);
return {
title: `${product.name} | ECサイト名`,
description: product.description.slice(0, 160),
openGraph: {
title: product.name,
description: product.description,
images: [{ url: product.imageUrl }],
},
};
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProductStatic(params.id);
const structuredData = {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"image": product.imageUrl,
"description": product.description,
"offers": {
"@type": "Offer",
"priceCurrency": "JPY",
"price": product.price,
"availability": "https://schema.org/InStock", // 静的な基本値
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
<ProductInfo product={product} />
</>
);
}
注意点:
generateMetadataはビルド時に実行されるため、動的データは含められない- 在庫状態など変動するデータは
availabilityを省略するか、クライアント側で更新する
動的在庫情報の後付け更新
構造化データのoffers.availabilityを動的に更新したい場合、クライアント側でJSON-LDを追加します。
// components/StockStatus.tsx
'use client';
import { useEffect } from 'react';
export function StockStatus({ productId }: { productId: string }) {
useEffect(() => {
fetch(`/api/stock/${productId}`)
.then((res) => res.json())
.then((data) => {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.textContent = JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"offers": {
"@type": "Offer",
"availability": data.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
},
});
document.head.appendChild(script);
});
}, [productId]);
return <div>在庫確認中...</div>;
}
ビルド時間とコストの最適化
PPRはビルド時に全ページを生成する必要がないため、大規模Eコマースサイトでビルド時間を大幅に削減できます。
generateStaticParamsとの併用
従来のISR(Incremental Static Regeneration)では、全商品ページを事前生成すると10万商品で数時間かかる場合がありました。PPRでは人気商品のみ静的生成し、残りはオンデマンド生成する戦略が有効です。
// app/products/[id]/page.tsx
export const experimental_ppr = true;
export const dynamicParams = true; // 未生成パスも許可
export async function generateStaticParams() {
// 人気商品トップ1000のみビルド時に生成
const popularProducts = await fetchPopularProducts(1000);
return popularProducts.map((p) => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
// 静的生成されていないページは初回アクセス時に生成
const product = await fetchProductStatic(params.id);
return <ProductInfo product={product} />;
}
ビルド時間の比較(10万商品の場合):
| 方式 | ビルド時間 | 初回表示速度 |
|---|---|---|
| 全ページSSG | 4〜6時間 | 高速(即座) |
| 全ページSSR | 30秒 | 遅い(600〜800ms) |
| PPR(人気1000のみ生成) | 5分 | 高速(200〜300ms) |
Edge Runtimeとの組み合わせ
動的部分をEdge Runtimeで処理すると、レイテンシをさらに削減できます。
// app/products/[id]/page.tsx
export const runtime = 'edge'; // Edge Runtimeを使用
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProductStatic(params.id);
return (
<>
<ProductInfo product={product} />
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} /> {/* Edgeで動的生成 */}
</Suspense>
</>
);
}
実装時の注意点とトラブルシューティング
Suspenseの配置ミス
PPRではSuspenseで囲んだ部分が動的になります。SEO重要要素を誤ってSuspense内に配置すると、クローラーが認識できません。
❌ 誤った実装:
export default async function ProductPage({ params }: { params: { id: string } }) {
return (
<Suspense fallback={<Skeleton />}>
{/* ❌ h1やメタ情報が動的になりSEOに悪影響 */}
<h1>{product.name}</h1>
<ProductInfo product={product} />
</Suspense>
);
}
✅ 正しい実装:
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProductStatic(params.id); // Suspense外で取得
return (
<>
{/* ✅ 静的部分: SEO重要要素 */}
<h1>{product.name}</h1>
<ProductInfo product={product} />
{/* ✅ 動的部分: ユーザー体験向上 */}
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
</>
);
}
キャッシュ戦略の見直し
PPRは静的部分をCDNでキャッシュしますが、動的部分のキャッシュ戦略も設計が必要です。
// app/api/stock/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const stock = await fetchStockFromDB(params.id);
return Response.json(stock, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120', // 60秒キャッシュ
},
});
}
開発環境での動作確認
PPRは本番ビルド時のみ有効です。npm run devでは動作しないため、確認はnpm run build && npm startで行います。
# ローカルでPPRをテスト
npm run build
npm start
まとめ
Next.js 15のPartial Pre-renderingは、EコマースサイトのSEOとパフォーマンスを同時に最適化する強力な手法です。
重要ポイント:
- 商品情報など静的コンテンツはビルド時に生成し、在庫状態など動的データはStreaming SSRで後から挿入
- 構造化データ・メタタグは必ず静的部分に配置し、クローラーが認識できるようにする
generateStaticParamsで人気商品のみ事前生成し、ビルド時間を短縮Suspenseの配置ミスに注意し、SEO重要要素を動的部分に含めない- Edge Runtimeとの併用で動的部分のレイテンシを最小化
PPRを活用することで、10万商品規模のEコマースサイトでも高速な初期表示とリッチなユーザー体験を両立できます。