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(); // 本物の404return (<Suspense fallback={<PostSkeleton />}><PostContent slug={slug} /></Suspense>);}
ストリーミングがCore Web Vitalsに与える影響
- TTFBが下がります — レイアウトのレンダリング時間程度まで。
- LCPは悪化することがあり、改善しません。 LCP要素は静的シェルに保持してください。
- CLSはあなたの責任です。
- INPはSelective Hydrationにより改善されます。
落とし穴とベストプラクティス
- Client Componentのレンダリング中にPromiseを作成しない。
- Suspenseは待機をキャッチし、エラーはキャッチしません。 各バウンダリをError Boundaryとペアにする。
- バウンダリは人が感じるようにサイズを決める。
- ボットはストリームを受け取りません。
- Promiseプロップはネットワークを通過します。
まとめ
シーケンス — リクエスト、待機、レンダリング、リクエスト、待機、レンダリング — の代わりに、並行ストリームが得られます:すべてが一度に始まり、ページの各部分はそのデータが届いた瞬間に表示されます。