ReactNext.js

React use()とSuspense:ウォーターフォールなしのデータストリーミング

React use()とSuspense:ウォーターフォールなしのデータストリーミング

ウォーターフォールには醜い算術的な特性があります。4つの300msリクエストが連鎖していると、300msで届いたかもしれないデータのために1.2秒の空白画面になります。

サスペンドされたコンポーネントが実際にすること

準備ができていないコンポーネントはPromiseをスローします。エラーではなく、Promiseを。ReactはそれをError Boundaryがエラーを捕まえるように捕まえ、部分的にレンダリングされた作業を破棄し、最も近いフォールバックを表示し、スローされたPromiseを購読します。それが解決すると、Reactはコンポーネントを最初から再実行します。

PromiseはこれらのRunの間で安定している必要があります。すべての実行で新しいPromiseは無限のサスペンションです。これがuse()を壊す最も一般的な方法です。

tsx

"use client";
import { use } from "react";
export default function Posts({ posts }) {
// promiseが解決するまでサスペンド
const allPosts = use(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Suspense、静的シェル、間違った順序で届くHTML

Reactは静的シェルを即座に送信します。遅延したピースはドキュメントの末尾に、見えない場所に追加され、Reactはその隣に小さなインライン<script>をストリームして、コンテンツを正しい場所に移動させます。

tsx

import { Suspense } from "react";
export default function Page() {
return (
<main>
<h1>ダッシュボード</h1> {/* 即座に送信 */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<Feed />
</Suspense>
</main>
);
}

早く始め、遅く読む

tsx

import { Suspense } from "react";
import Posts from "@/app/ui/posts";
import Comments from "@/app/ui/comments";
export default function Page() {
// 両方のリクエストが今、並行して開始
const posts = getPosts();
const comments = getComments();
return (
<>
<Suspense fallback={<div>投稿を読み込み中...</div>}>
<Posts posts={posts} />
</Suspense>
<Suspense fallback={<div>コメントを読み込み中...</div>}>
<Comments comments={comments} />
</Suspense>
</>
);
}

誰も予期しない落とし穴:ストリーミングはHTTPステータスコードを使用する

Suspenseフォールバックがレンダリングされてストリームが開くと、サーバーはすでに200 OKにコミットしています。ストリームされたバウンダリ内でトリガーされるnotFound()は本物の404を返せません。

tsx

export default async function PostPage({ params }) {
const { slug } = await params;
const exists = await checkSlugExists(slug); // 素早く、ストリームが開く前に
if (!exists) notFound(); // 本物の404
return (
<Suspense fallback={<PostSkeleton />}>
<PostContent slug={slug} />
</Suspense>
);
}

ストリーミングがCore Web Vitalsに与える影響

  • TTFBが下がります — レイアウトのレンダリング時間程度まで。
  • LCPは悪化することがあり、改善しません。 LCP要素は静的シェルに保持してください。
  • CLSはあなたの責任です。
  • INPはSelective Hydrationにより改善されます。

落とし穴とベストプラクティス

  • Client Componentのレンダリング中にPromiseを作成しない。
  • Suspenseは待機をキャッチし、エラーはキャッチしません。 各バウンダリをError Boundaryとペアにする。
  • バウンダリは人が感じるようにサイズを決める。
  • ボットはストリームを受け取りません。
  • Promiseプロップはネットワークを通過します。

まとめ

シーケンス — リクエスト、待機、レンダリング、リクエスト、待機、レンダリング — の代わりに、並行ストリームが得られます:すべてが一度に始まり、ページの各部分はそのデータが届いた瞬間に表示されます。