reactnext.js

React use() en Suspense: gegevensstreaming zonder watervallen

React use() en Suspense: gegevensstreaming zonder watervallen

Een waterval heeft een lelijke rekenkundige eigenschap. Vier verzoeken van 300ms in een keten zijn 1,2 seconde leeg scherm voor gegevens die in 300ms hadden kunnen aankomen.

Wat een gesuspend component echt doet

Een component die niet klaar is, gooit een Promise. Geen fout, een Promise. React vangt het op, verwerpt het gedeeltelijk gerenderde werk, toont de dichtstbijzijnde fallback en abonneert op de gegooid Promise. Wanneer die oplost, voert React het component opnieuw uit vanaf het begin.

De Promise moet stabiel zijn tussen die uitvoeringen. Als het opnieuw uitvoeren een gloednieuwe Promise aanmaakt, is dat een oneindige suspensie. Dit is de meest voorkomende manier om use() te breken.

tsx

"use client";
import { use } from "react";
export default function Posts({ posts }) {
// suspendeert totdat de promise oplost
const allPosts = use(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Suspense, de statische shell en HTML die buiten volgorde aankomt

React stuurt de statische shell onmiddellijk. Het late stuk wordt aan het einde van het document toegevoegd, buiten het zicht, en React streamt een klein inline-<script> ernaast om de inhoud naar de juiste plaats te verplaatsen.

tsx

import { Suspense } from "react";
export default function Page() {
return (
<main>
<h1>Dashboard</h1> {/* direct verstuurd */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<Feed />
</Suspense>
</main>
);
}

Vroeg starten, laat lezen

tsx

import { Suspense } from "react";
import Posts from "@/app/ui/posts";
import Comments from "@/app/ui/comments";
export default function Page() {
// beide verzoeken starten nu, parallel
const posts = getPosts();
const comments = getComments();
return (
<>
<Suspense fallback={<div>Berichten laden...</div>}>
<Posts posts={posts} />
</Suspense>
<Suspense fallback={<div>Reacties laden...</div>}>
<Comments comments={comments} />
</Suspense>
</>
);
}

De valkuil die niemand verwacht: streaming verbruikt uw HTTP-statuscode

Zodra een Suspense-fallback rendert en de stream opent, heeft de server al 200 OK vastgelegd. Een notFound() dat binnen een gestreamde grens wordt geactiveerd, kan niet een echte 404 geven.

tsx

export default async function PostPage({ params }) {
const { slug } = await params;
const exists = await checkSlugExists(slug); // snel, voor de stream opent
if (!exists) notFound(); // echte 404
return (
<Suspense fallback={<PostSkeleton />}>
<PostContent slug={slug} />
</Suspense>
);
}

Wat streaming doet met Core Web Vitals

  • TTFB daalt naar de rendertijd van de layout.
  • LCP kan verslechteren, niet verbeteren. LCP-elementen in de statische shell houden.
  • CLS is uw verantwoordelijkheid.
  • INP verbetert dankzij selectieve hydration.

Valkuilen en best practices

  • Nooit de Promise aanmaken tijdens het renderen van een Client Component.
  • Suspense vangt wachten, niet falen. Elke grens koppelen aan een Error Boundary.
  • De grens dimensioneren zoals een persoon zou doen.
  • Bots ontvangen de stream niet.
  • Een Promise-prop overschrijdt het netwerk.

Conclusie

In plaats van een reeks — verzoek, wachten, renderen, verzoek, wachten, renderen — krijg je parallelle streams: alles start tegelijk, en elk deel van de pagina verschijnt zodra zijn gegevens aankomen.