React Server Components パフォーマンス最適化:データフェッチとキャッシュの実践
もしあなたの RSC ページの TTFB がまだ 300-500ms のままであれば、その性能ポテンシャルの 30% 程度しか引き出せていないかもしれません。実測データによると、適切なストリーミングアーキテクチャを使用することで TTFB を 45ms まで下げることができます。これは魔法ではありません。React Server Components のストリーミングレンダリングが真に有効化された結果です。
正直に言うと、私もこの落とし穴にはまりました。昨年、ある EC チームの商品詳細ページの最適化を手伝った際、彼らは Next.js App Router を使用していましたが、TTFB は 380ms 程度で安定していました。調査の結果、ネストされたコンポーネントがそれぞれデータを取得し、典型的な「ウォーターフォール」が形成されていることが判明しました。商品情報がレビューを待ち、レビューが価格データを待ち、価格が在庫確認を待つ状態です。9 秒間のホワイトスクリーン。
この記事では、この問題をどう解決するかについてお話しします。4つのウォーターフォール対処方法を比較し、5つのキャッシュ API の使用シーンを詳しく解説し、そのままコピーできる設定テンプレートを提供します。TTFB を 450ms から 45ms まで下げるには、数個の Suspense 境界の位置調整だけが必要かもしれません。
ウォーターフォール問題:RSC パフォーマンスの最大の敵
まず実例を見てみましょう。EC サイトで商品詳細ページを開くと、ページはまず商品名を表示し、3秒待って価格が現れ、さらに5秒待ってコメント欄が表示されます。ユーザー体験は?災難です。
これがウォーターフォール問題です。ネストされたコンポーネントがそれぞれデータを取得し、並列ではなく順次実行されます。React Server Components のデータフェッチはデフォルトで同期的にブロックされます。await を含むリクエストはすべてレンダリングをブロックします。Suspense で囲まない限り。
ウォーターフォールの2つの形態
1つ目:Server 内部ウォーターフォール。同一ページ内で、親コンポーネントがデータを取得した後に子コンポーネントをレンダリングし、子コンポーネントが自分のデータを取得します。典型的なコード:
// ウォーターフォールの例——これは問題のあるコードです
async function ProductPage({ id: string }) {
// 最初のリクエスト:1秒
const product = await db.getProduct(id);
// 子コンポーネントがレンダリングされた後にこれらのリクエストが開始
return (
<div>
<ProductDetails product={product} />
<ProductPrice id={id} /> {/* 内部で await getPrice(id)、3秒 */}
<ProductReviews id={id} /> {/* 内部で await getReviews(id)、5秒 */}
</div>
);
}
// ProductPrice.tsx
async function ProductPrice({ id }) {
const price = await getPrice(id); // 親コンポーネントのレンダリング後に実行
return <span>{price}</span>;
}
総所要時間?9秒。ユーザーは空白のページを9秒間見つめることになります。
2つ目:Client-Server ウォーターフォール。クライアントコンポーネントがサーバーにリクエストし、サーバーがさらにデータベースにリクエストします。これはより隠密で、React DevTools Profiler でのみ捉えられます。N+1 問題の変種です。
ウォーターフォールの見分け方
React DevTools Profiler を開き、ページロードを1回録画します。タイムラインに明確な階段状のリクエスト分布があれば、各リクエストが前の完了を待っている状態、それがウォーターフォールです。
より直感的な方法もあります。ブラウザの Network パネルを開き、リクエストの開始時間を見ます。データリクエストが集中的ではなく分散して発行されている場合、問題はほぼ確定です。
興味深いことに、多くの開発者は RSC を使用すれば自動的にパフォーマンス向上が得られると思っています。実際にはそうではありません。SitePoint 2026年のレポートによると、ほとんどのチームは RSC のパフォーマンスポテンシャルの約30%しか引き出せていません。原因はウォーターフォールへの対処不足です。
4つの解決方法比較:荒削りからエレガントまで
ウォーターフォール問題には4つの主流な解決方法があります。シンプルから複雑、荒削りからエレガントまで。
方法1:Promise.all 並列フェッチ
最も直接的なアプローチです。すべてのリクエストを同時に発行し、Promise.all ですべての完了を待ちます。
// 方法1:Promise.all 並列フェッチ
async function ProductPage({ id: string }) {
// すべてのリクエストを同時に発行
const [product, price, reviews] = await Promise.all([
getProduct(id), // 1秒
getPrice(id), // 3秒
getReviews(id), // 5秒
]);
return (
<div>
<ProductDetails product={product} />
<ProductPriceDisplay price={price} />
<ProductReviewsList reviews={reviews} />
</div>
);
}
総所要時間?5秒。最も遅いリクエストが全体の時間を決定します。
メリット:シンプル、変更が少ない。
デメリット:ユーザーは最も遅いリクエストの完了を待たないと何も見られません。もう1つの問題はデータ結合です。親コンポーネントが子コンポーネントに必要なデータを知る必要があり、コンポーネントの独立性の原則に違反します。
方法2:Suspense 境界分離
データ依存部分に Suspense を追加し、重要なコンテンツを先に表示します。
// 方法2:Suspense 境界分離
async function ProductPage({ id: string }) {
const product = await getProduct(id); // 重要データを先に待つ
return (
<div>
<ProductDetails product={product} /> {/* 1秒後に表示 */}
{/* 重要でない部分を Suspense で囲む */}
<Suspense fallback={<PriceSkeleton />}>
<ProductPrice id={id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={id} />
</Suspense>
</div>
);
}
ユーザー体験:1秒で商品情報を見られ、3秒で価格が現れ、5秒でコメントの読み込みが完了します。
メリット:重要なコンテンツが優先的に表示され、ユーザー体験が良い。
デメリット:データリクエストは依然として順次発行される。ProductPrice と ProductReviews のリクエストは親コンポーネントのレンダリング後に開始され、真の並列ではありません。
方法3:Promise を Props として渡す
親コンポーネントですべてのリクエストを開始し、Promise を props として子コンポーネントに渡します。子コンポーネントは自分で await します。
// 方法3:Promise 渡しパターン
async function ProductPage({ id: string }) {
// すべてのリクエストを即座に開始、await しない
const productPromise = getProduct(id);
const pricePromise = getPrice(id);
const reviewsPromise = getReviews(id);
// 重要データのみ await
const product = await productPromise;
return (
<div>
<ProductDetails product={product} />
<Suspense fallback={<PriceSkeleton />}>
<ProductPrice pricePromise={pricePromise} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews reviewsPromise={reviewsPromise} />
</Suspense>
</div>
);
}
// ProductPrice.tsx——Promise を受け取る
async function ProductPrice({ pricePromise }) {
const price = await pricePromise; // 親コンポーネントが開始した Promise を再利用
return <span>{price}</span>;
}
3つのリクエストが親コンポーネントで同時に開始されます。重要データは1秒で表示され、価格は3秒、コメントは5秒です。
メリット:すべてのリクエストが並列で発行され、重要なコンテンツが優先的に表示され、データが分離される(子コンポーネントは Promise のみ受け取る)。
デメリット:コンポーネントのインターフェースを変更する必要があり、子コンポーネントは id から Promise を受け取るように変わる。
方法4:React cache() + preload(推奨)
React 19 で cache() API が導入されました。preload パターンと組み合わせることで、最もエレガントな解決方法になります。
// 方法4:React cache() + preload
import { cache } from 'react';
// cache でデータフェッチ関数をラップ
const getComments = cache(async (postId: string) => {
return db.getComments(postId);
});
// preload 関数をエクスポート、用途を明確に示す
export const preloadComments = (id: string) => {
void getComments(id); // await しない、開始だけする
};
// 親コンポーネント
async function PostPage({ postId: string }) {
preloadComments(postId); // コメントをプリロード
const post = await getPost(postId); // 重要データのみ await
return (
<div>
<PostContent post={post} />
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={postId} /> {/* id をそのまま使用、cache が自動再利用 */}
</Suspense>
</div>
);
}
// Comments.tsx——コンポーネントインターフェースは変更なし
async function Comments({ postId }) {
const comments = await getComments(postId); // preload の Promise を再利用
return <CommentList comments={comments} />;
}
仕組み:cache() 関数は同一レンダリングサイクル内で自動的にメモ化されます。preload 呼び出し時にリクエストがトリガーされますが待機せず、子コンポーネントが await する際に同じ Promise を再利用します。
メリット:
- コンポーネントインターフェースが変わらない(引き続き id を渡す)
- リクエストが自動的にメモ化され、データ結合なし
- 子コンポーネント削除時、preload が無用なコードになり、発見しやすい
デメリット:cache() の仕組みを理解する必要があり、隠れた結合に注意(Comments コンポーネント削除時に preload も削除)。
4つの方法の比較
| 方法 | 総所要時間 | 重要コンテンツ表示時間 | データ結合 | 変更コスト |
|---|---|---|---|---|
| 順次取得 | 9s | 9s | なし | なし |
| Promise.all | 5s | 5s | あり | 小 |
| Suspense | 5s | 1s | なし | 小 |
| Promise 渡し | 5s | 1s | 解消 | 中 |
| cache() + preload | 1s | 1s | なし | 中 |
実際の選択はチームの状況によります。迅速な移行には方法2を、新規プロジェクトには方法4を推奨します。
ストリーミングレンダリングアーキテクチャ:TTFB 45ms の秘密
従来の SSR のワークフローはこのようになります。すべてのデータフェッチの完了を待ち、完全な HTML をレンダリングし、ブラウザに一度に送信します。TTFB(Time to First Byte)は、データフェッチ時間にレンダリング時間を加えたものです。
具体的な数字:データベースクエリ 400ms、レンダリング 50ms、TTFB は約 450ms。ユーザーは約半秒間空白のページを見つめることになります。
RSC Streaming がこのフローをどう変えるか
ストリーミングレンダリングの核心は、何かを減らすことではなく、コンテンツがユーザーに届く順序を変えることです。静的部分は即座に送信され、動的部分はストリーミングで補完されます。
// ストリーミングアーキテクチャの例
export default async function Dashboard() {
return (
<Layout> {/* 静的 shell、Suspense で囲まない */}
<Nav /> {/* 即座にレンダリング */}
<Sidebar /> {/* 即座にレンダリング */}
<Suspense fallback={<ChartSkeleton />}>
<DynamicChart /> {/* 動的データ、ストリーミング読み込み */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable /> {/* 動的データ、ストリーミング読み込み }}
</Suspense>
</Layout>
);
}
ワークフローの分解:
- T=0ms:静的 shell(Layout、Nav、Sidebar)が CDN エッジキャッシュから即座に送信
- T=30-50ms:ブラウザが静的 shell のレンダリングを開始、スケルトンを表示
- T=200ms:DynamicChart のデータフェッチ完了、対応する Suspense 境界のコンテンツがストリーミング送信
- T=400ms:DataTable のデータフェッチ完了、対応するコンテンツがストリーミング送信
TTFB?約 45ms。静的 shell の送信時間です。
PPR(Partial Prerendering)の役割
PPR は Next.js 15 で導入された機能で、Next.js 16 ではデフォルトで有効になります。静的部分を CDN にプリレンダリングし、動的部分はストリーミングを維持します。
設定方法:
// next.config.js——Next.js 15
module.exports = {
experimental: {
ppr: true, // PPR を有効化
},
};
// next.config.js——Next.js 16(プレビュー)
module.exports = {
experimental: {
ppr: 'incremental', // 段階的に有効化
cacheComponents: true, // 新しいキャッシュモデル
},
};
PPR を有効にすると、静的 shell(ナビゲーション、レイアウト、スケルトン)が CDN にプリレンダリングされて保存されます。ユーザーがアクセスすると、CDN は即座に静的 HTML を返し、動的部分はサーバーからストリーミングで補完されます。
Suspense 境界設計の原則
重要な原則:Suspense でストリーミングブロックをマークし忘れると、React はアプリケーション全体を1つの巨大なブロックとして扱います。
正しいアプローチ:
- 静的部分は Suspense で囲まない:ナビゲーション、Layout、データに依存しないスケルトン
- 動的部分は Suspense で囲む:データベースや API に依存するコンポーネント
// 正しい例
export default async function Page() {
return (
<>
<Header /> {/* 静的、囲まない */}
<main>
<Suspense fallback={<HeroSkeleton />}>
<HeroSection /> {/* 動的、囲む */}
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent /> {/* 動的、囲む */}
</Suspense>
</main>
<Footer /> {/* 静的、囲まない */}
</>
);
}
間違った例(ページ全体がブロック):
// 間違った例——Suspense 忘れ
export default async function Page() {
const data = await fetchDashboard(); // await がページ全体をブロック
return (
<>
<Header />
<Dashboard data={data} />
<Footer />
</>
);
}
Suspense 境界がないと、ページ全体が1つのストリーミングブロックとして扱われます。TTFB は依然として 450ms です。
パフォーマンスデータの比較
| レンダリングモード | TTFB | LCP | 説明 |
|---|---|---|---|
| 従来の SSR | ~450ms | ~500ms | すべてのデータを待つ |
| RSC(Suspense なし) | ~450ms | ~500ms | 従来の SSR と同等 |
| RSC Streaming | ~45ms | ~200ms | 静的 shell を即座に送信 |
| RSC + PPR | ~30ms | ~150ms | CDN が静的 shell をキャッシュ |
データ出典:SitePoint 2026年レポート、実測データはデータソースや CDN 設定により異なる可能性があります。
5つのキャッシュ API 使用ガイド
Next.js と React は5つのキャッシュメカニズムを提供しています。正しく選べば効果倍増、間違えれば重複リクエストが発生する可能性があります。
1. fetch cache(最も一般的)
fetch リクエストは Server Components で自動的にメモ化されます。同一レンダリングサイクル内で、同じ URL とパラメータのリクエストは一度だけ発行されます。
// fetch cache の例
async function ProductCard({ id }) {
// 自動キャッシュ、同じ URL は重複リクエストしない
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'force-cache', // 強制キャッシュ(デフォルト)
next: {
revalidate: 3600, // 1時間後に再検証
tags: ['products'], // タグ、revalidateTag で使用
},
});
return <Card data={res.json()} />;
}
async function ProductList() {
// このリクエストは上記のキャッシュを再利用
const res = await fetch('https://api.example.com/products', {
next: { tags: ['products'] },
});
return <List data={res.json()} />;
}
設定オプション:
cache: 'force-cache':キャッシュ優先(デフォルト)cache: 'no-store':毎回リクエストnext.revalidate:定期的な再検証(秒)next.tags:タグ、revalidateTag で手動リフレッシュ
2. React cache()(React 19 新規)
関数呼び出し結果のキャッシュに使用します。データベースクエリ、カスタムデータフェッチ関数に適しています。
import { cache } from 'react';
// cache でデータベースクエリをラップ
export const getUser = cache(async (id: string) => {
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
return user;
});
// 複数のコンポーネントで使用、自動的にメモ化
async function UserProfile({ id }) {
const user = await getUser(id);
return <Profile user={user} />;
}
async function UserStats({ id }) {
const user = await getUser(id); // 上記の結果を再利用
return <Stats user={user} />;
}
注意:cache() は同一レンダリングサイクル内でのみ有効です。リクエストを跨ぐには unstable_cache を使用します。
3. unstable_cache(Next.js 14-15)
永続キャッシュ、リクエストを跨いで保持します。高価な計算、ページを跨ぐデータ共有に適しています。
import { unstable_cache } from 'next/cache';
// 関数をラップ、永続キャッシュを追加
export const getPopularProducts = unstable_cache(
async () => {
const products = await db.getPopularProducts();
return products;
},
['popular-products'], // キャッシュキー
{
revalidate: 3600, // 1時間で再検証
tags: ['products', 'popular'], // 複数タグ
}
);
// 使用
async function HomePage() {
const products = await getPopularProducts();
return <ProductGrid products={products} />;
}
手動リフレッシュ:
import { revalidateTag } from 'next/cache';
// Server Action または API Route 内で
async function updateProduct() {
await db.updateProduct();
await revalidateTag('products'); // すべての products タグのキャッシュをリフレッシュ
}
4. use cache(Next.js 16 新規)
コンポーネントレベルのキャッシュディレクティブ。関数またはコンポーネントの先頭に ‘use cache’ を追加すると、出力が自動的にキャッシュされます。
// 関数レベル 'use cache'
'use cache';
export async function getRecommendations(userId: string) {
return db.getRecommendations(userId);
}
// コンポーネントレベル 'use cache'
'use cache';
export async function CachedFooter() {
const links = await getFooterLinks();
return <Footer links={links} />;
}
適用シーン:頻繁にアクセスされるコンポーネント、静的コンテンツ。実験的機能、Next.js 16 で正式サポート。
5. revalidatePath / revalidateTag
手動でキャッシュをリフレッシュする方法。
import { revalidatePath, revalidateTag } from 'next/cache';
// パスでリフレッシュ
await revalidatePath('/products'); // そのパスのすべてのキャッシュをリフレッシュ
await revalidatePath('/products/[id]', 'page'); // 特定のページをリフレッシュ
// タグでリフレッシュ
await revalidateTag('products'); // すべての products タグのキャッシュをリフレッシュ
選択の推奨:
- 精密な制御には revalidateTag(推奨)
- 一括リフレッシュには revalidatePath
キャッシュ API の比較
| API | キャッシュ範囲 | 永続化 | 適用シーン | バージョン |
|---|---|---|---|---|
| fetch cache | 単一リクエスト | 設定可能 | API リクエスト | Next.js 13+ |
| React cache() | 単一レンダリングサイクル | なし | データベースクエリ、カスタム関数 | React 19 |
| unstable_cache | リクエスト間 | あり | 高価な計算、共有データ | Next.js 14-15 |
| use cache | 関数/コンポーネントレベル | あり | 頻繁にアクセスされるコンポーネント | Next.js 16 |
| Cache Components | コンポーネント出力 | あり | PPR と組み合わせ | Next.js 16 |
実践設定テンプレートとよくある落とし穴
理論は説明したので、すぐにコピーできる設定を提供します。
next.config.js 完全設定
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// Next.js 15:PPR を有効化
ppr: true,
// Next.js 16:新しいキャッシュモデル
// cacheComponents: true, // 正式版で有効化
},
// パフォーマンス関連
images: {
formats: ['image/avif', 'image/webp'],
},
// 出力最適化
output: 'standalone', // Docker デプロイ用
};
export default nextConfig;
preload 関数エクスポートの規範
preload 関数は隠れた結合を生みやすいです。深い階層の子コンポーネントを削除する際、preload が無用なコードになる可能性があります。
推奨:preload 関数の上にコメントを追加し、用途を明記します。
// comments.ts
import { cache } from 'react';
const getComments = cache(async (postId: string) => {
return db.getComments(postId);
});
/**
* preloadComments:Comments コンポーネントのコメントデータをプリロード
* 注意:Comments コンポーネント削除時はこの preload 関数も削除すること
*/
export const preloadComments = (id: string) => {
void getComments(id);
};
export async function Comments({ postId }) {
const comments = await getComments(postId);
return <CommentList comments={comments} />;
}
よくある落とし穴事例
落とし穴1:Suspense 境界忘れ
症状:ページ全体の TTFB が依然として 450ms、ストリーミング効果なし。
原因:Suspense 境界がないと、React はページ全体を1つのストリーミングブロックとして扱う。
解決:データ依存コンポーネントに Suspense を追加。
// 修正前
async function Page() {
const data = await getData(); // ページ全体をブロック
return <Dashboard data={data} />;
}
// 修正後
async function Page() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
);
}
落とし穴2:preload したが未使用
症状:リクエストが発行されたがデータが使われず、リソースの無駄遣い。
原因:子コンポーネントを削除したが preload 関数は残した。
解決:コンポーネント削除時に preload も削除するか、コメントで関連性をマークする。
落とし穴3:キャッシュ tags の競合
症状:revalidateTag のリフレッシュ範囲が広すぎ、リフレッシュすべきでないデータもリフレッシュされる。
原因:関連性のない複数のキャッシュが同じ tag を使用している。
解決:異なるビジネスのデータには異なる tag を使用する。
// 間違った例
await fetch(url, { next: { tags: ['data'] } }); // すべてのデータが 'data' tag
await revalidateTag('data'); // すべてのデータキャッシュをリフレッシュ
// 正しい例
await fetch(productsUrl, { next: { tags: ['products'] } });
await fetch(usersUrl, { next: { tags: ['users'] } });
await revalidateTag('products'); // products のみリフレッシュ
落とし穴4:fetch cache と React cache の混用
症状:同じデータで2回リクエストが発生する。
原因:fetch で ‘no-store’ を使用、React cache() が再利用できない。
解決:fetch は force-cache またはデフォルト設定を使用し、React cache が再利用できるようにする。
// 間違った例
const data1 = await fetch(url, { cache: 'no-store' }); // キャッシュしない
const data2 = await getData(); // React cache でラップ、再利用不可
// 正しい例
const data1 = await fetch(url); // デフォルト force-cache
const data2 = await getData(); // 再利用可能
デバッグツール
- React DevTools Profiler:レンダリングプロセスを録画、ウォーターフォール分布を確認
- Next.js 分析ツール:
next build --experimental-debugでビルド分析を出力 - Chrome DevTools:Network パネルでリクエストタイミングを確認、Performance パネルでレンダリングタイミングを確認
重要な指標:
- TTFB:First Byte Time、目標 < 100ms
- LCP:Largest Contentful Paint、目標 < 2.5s
- CLS:Cumulative Layout Shift、目標 < 0.1
移行の推奨
既存の SSR ページから RSC ストリーミングへの移行:
- まずウォーターフォールを特定:Profiler で録画、順次データフェッチを見つける
- Suspense 境界を追加:データ依存コンポーネントに Suspense を追加、重要パスを優先
- preload を追加:深いコンポーネントに cache() + preload を使用
- キャッシュを設定:fetch に tags を追加、精密なリフレッシュ制御を実現
段階的に移行し、一度にリファクタリングしない。まず最も遅いページを処理し、効果を測定してから拡大する。
まとめ
これだけ説明しましたが、核心は3つのステップです。ウォーターフォールを特定し、解決方法を選択し、ストリーミングアーキテクチャを設定する。
ウォーターフォール問題は簡単に特定できます。ネストされたコンポーネントがそれぞれデータを await し、タイムライン上で階段状に分布しています。4つの解決方法はそれぞれ適用シーンがあります。迅速な移行には Suspense 境界を、新規プロジェクトには React cache() + preload を使用します。
ストリーミングアーキテクチャの鍵は Suspense 境界の位置です。静的部分(ナビゲーション、Layout)は囲まず、動的部分(データ依存コンポーネント)は必ず囲みます。この境界を忘れると、ページ全体がブロックレンダリングされます。
パフォーマンス向上は定量化できます。TTFB は 450ms から 45ms へ、10倍の差です。間にあるのは数個の Suspense 境界と1つの preload 関数だけです。
今すぐ Next.js プロジェクトを開き、ネストされたコンポーネントがそれぞれデータを取得していないか確認してください。TTFB がまだ 300ms 以上あるなら、重要なコンテンツを Suspense で囲んでみてください。ユーザーはすぐに違いを感じるはずです。
React Server Components パフォーマンス最適化プロセス
ウォーターフォール特定からストリーミングアーキテクチャ設定までの完全な最適化ステップ
⏱️ 目安時間: 60 分
- 1
ステップ1: ウォーターフォール問題を特定
React DevTools Profiler を使用してページロードを録画:
• Chrome DevTools を開き、Profiler タブに切り替える
• 録画をクリック、ページを更新、ロード完了を待つ
• タイムライン上のリクエスト分布を確認
• 階段状の分布 = ウォーターフォール問題
• TTFB が 300ms を超えていないか確認 - 2
ステップ2: 解決方法を選択
チームの状況に応じて選択:
• 迅速な移行:方法2(Suspense 境界分離)
• 新規プロジェクト:方法4(React cache() + preload)
• データ結合を許容:方法1(Promise.all)
• コンポーネントインターフェース維持:方法4(推奨) - 3
ステップ3: Suspense 境界を追加
データ依存コンポーネントに Suspense を追加:
• 静的部分は囲まない(ナビゲーション、Layout)
• 動的部分は必ず囲む(データ依存コンポーネント)
• 適切な fallback スケルトンを提供
• 重要パスを優先的に処理 - 4
ステップ4: React cache() + preload を設定
React 19 cache() API を使用:
• cache でデータフェッチ関数をラップ
• preload 関数をエクスポート、await しない
• コメントで関連性をマーク
• コンポーネント削除時に preload も削除 - 5
ステップ5: キャッシュ戦略を設定
適切なキャッシュ API を選択:
• fetch cache:API リクエスト(最も一般的)
• React cache():データベースクエリ
• unstable_cache:リクエスト間共有
• キャッシュに tags を追加、精密なリフレッシュを実現 - 6
ステップ6: パフォーマンス向上を測定
最適化効果を検証:
• TTFB 目標:< 100ms
• LCP 目標:< 2.5s
• CLS 目標:< 0.1
• 最適化前後のパフォーマンスデータを比較
FAQ
React Server Components のウォーターフォール問題とは何ですか?
4つのウォーターフォール解決方法をどう選択しますか?
• Promise.all:シンプルで直接、迅速な修正に適するが、データ結合あり
• Suspense 境界:重要コンテンツを優先表示、迅速な移行に適する
• Promise 渡し:すべてのリクエストが並列、コンポーネントインターフェース変更可能な場合に適する
• React cache() + preload:最もエレガントな方法、新規プロジェクトに適する(推奨)
Suspense 境界はどこに配置すべきですか?
5つのキャッシュ API の違いは何ですか?
• fetch cache:API リクエスト、自動メモ化(Next.js 13+)
• React cache():データベースクエリ、単一レンダリングサイクルキャッシュ(React 19)
• unstable_cache:リクエスト間永続化、高価な計算(Next.js 14-15)
• use cache:関数/コンポーネントレベルキャッシュ(Next.js 16)
• revalidatePath/Tag:手動キャッシュリフレッシュ
RSC パフォーマンス最適化効果をどう測定しますか?
PPR(Partial Prerendering)とは何ですか?
よくあるキャッシュ設定の落とし穴は何ですか?
• Suspense 境界忘れ:ページ全体がブロック、TTFB 改善なし
• preload したが未使用:コンポーネント削除時に preload が残り、リクエストの無駄
• キャッシュ tags 競合:revalidateTag のリフレッシュ範囲が広すぎる
• fetch cache と React cache 混用:no-store 使用で再利用不可
参考資料
- Data Fetching Patterns and Best Practices — Next.js 公式ドキュメント
- React Server Components Streaming Performance Guide 2026 — SitePoint、2026-02
- Avoiding Server Component Waterfall Fetching with React 19 cache() — Aurora Scharff、2025-02
- Avoiding Waterfalls in React Server Components — Akhila Ariyachandra、2026-01
- Functions: revalidatePath — Next.js 公式ドキュメント
- Functions: revalidateTag — Next.js 公式ドキュメント
- Partial Prerendering (PPR) — Next.js 公式ドキュメント
- React v19 Release — React 公式ブログ
8 min read · 公開日: 2026年5月13日 · 更新日: 2026年5月13日
関連記事
Next.js App Router 入門ガイド:コアコンセプトと基本的な使い方を完全解説
Next.js App Router 入門ガイド:コアコンセプトと基本的な使い方を完全解説
Next.js 15実践:週末だけで生産性レベルのブログシステムを構築した話
Next.js 15実践:週末だけで生産性レベルのブログシステムを構築した話
Next.js Middleware 実践ガイド:パス一致、Edge Runtime 制限とよくある落とし穴

コメント
GitHubアカウントでログインしてコメントできます