React use() 與 Suspense:無瀑布的資料串流
React use() 與 Suspense:無瀑布的資料串流
瀑布有一個醜陋的算術特性。四個 300ms 請求串聯在一起,對於本可以在 300ms 到達的資料,產生了 1.2 秒的空白螢幕。
暫停的元件實際上做了什麼
未準備好的元件拋出一個 Promise。不是錯誤,是 Promise。React 像 Error Boundary 捕獲錯誤一樣捕獲它,丟棄部分渲染的工作,顯示最近的後備,並訂閱被拋出的 Promise。當它解析時,React 從頭再次執行元件。
Promise 在這些執行之間必須保持穩定。每次執行都是新的 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 通過選擇性水化得到改善。
陷阱與最佳實踐
- 永遠不要在 Client Component 渲染期間建立 Promise。
- Suspense 捕獲等待,而不是錯誤。 將每個邊界與 Error Boundary 配對。
- 像人感知的那樣調整邊界大小。
- 機器人不接收串流。
- Promise prop 跨越網路傳遞。
總結
不再是序列——請求、等待、渲染、請求、等待、渲染——你得到並行串流:所有事情同時開始,頁面的每個部分在其自己的資料到達後立即顯示。