React use() och Suspense: dataströmning utan vattenfall
Ett vattenfall har en ful aritmetisk egenskap. Fyra 300ms-förfrågningar i en kedja är 1,2 sekunder med tom skärm för data som kunde ha anlänt på 300ms.
Vad en suspenderad komponent faktiskt gör
En komponent som inte är klar kastar ett Promise. Inte ett fel, ett Promise. React fångar det precis som en Error Boundary fångar ett fel, kasserar det delvis renderade arbetet, visar närmaste fallback och prenumererar på det kastade Promise. När det löser sig kör React komponenten igen från toppen.
Promise måste vara stabilt mellan dessa körningar. Ett nytt Promise vid varje körning är en oändlig suspension. Det är det vanligaste sättet att förstöra use().
tsx
"use client";import { use } from "react";export default function Posts({ posts }) {// suspenderar tills promise löser sigconst allPosts = use(posts);return (<ul>{allPosts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>);}
Suspense, det statiska skalet och HTML som anländer i fel ordning
React skickar det statiska skalet omedelbart. Den försenade biten läggs till i slutet av dokumentet, utom synhåll, och React strömmar ett litet inline <script> bredvid det för att flytta innehållet till rätt ställe.
tsx
import { Suspense } from "react";export default function Page() {return (<main><h1>Dashboard</h1> {/* skickas omedelbart */}<Suspense fallback={<StatsSkeleton />}><Stats /></Suspense><Suspense fallback={<FeedSkeleton />}><Feed /></Suspense></main>);}
Starta tidigt, läs sent
tsx
import { Suspense } from "react";import Posts from "@/app/ui/posts";import Comments from "@/app/ui/comments";export default function Page() {// båda förfrågningarna startar nu, parallelltconst posts = getPosts();const comments = getComments();return (<><Suspense fallback={<div>Laddar inlägg...</div>}><Posts posts={posts} /></Suspense><Suspense fallback={<div>Laddar kommentarer...</div>}><Comments comments={comments} /></Suspense></>);}
Fällan ingen förväntar sig: strömning använder din HTTP-statuskod
När en Suspense-fallback renderas och strömmen öppnas har servern redan förbundit sig till 200 OK. Ett notFound() som utlöses inuti en strömmad gräns kan inte ge en äkta 404.
tsx
export default async function PostPage({ params }) {const { slug } = await params;const exists = await checkSlugExists(slug); // snabbt, innan strömmen öppnasif (!exists) notFound(); // äkta 404return (<Suspense fallback={<PostSkeleton />}><PostContent slug={slug} /></Suspense>);}
Vad strömning gör med Core Web Vitals
- TTFB sjunker till ungefär rendertiden för layoutet.
- LCP kan försämras, inte förbättras. Håll LCP-element i det statiska skalet.
- CLS är ditt ansvar.
- INP förbättras tack vare selektiv hydration.
Fallgropar och bästa praxis
- Skapa aldrig Promise under rendering av en Client Component.
- Suspense fångar väntan, inte fel. Para varje gräns med en Error Boundary.
- Dimensionera gränsen som en person skulle göra.
- Botar tar inte emot strömmen.
- Ett Promise-prop passerar nätverket.
Slutsats
Istället för en sekvens — förfrågan, väntan, rendering, förfrågan, väntan, rendering — får du parallella strömmar: allt startar på en gång, och varje del av sidan visas så snart dess egna data anländer.