reactnext.js

React use() og Suspense: datastreaming uden vandfald

React use() og Suspense: datastreaming uden vandfald

Et vandfald har en grim aritmetisk egenskab. Fire 300ms-forespørgsler i en kæde er 1,2 sekunder med tomt skærm for data der kunne have ankommet på 300ms.

Hvad en suspenderet komponent faktisk gør

En komponent der ikke er klar, kaster et Promise. Ikke en fejl, et Promise. React fanger det som en Error Boundary fanger en fejl, kasserer det delvist renderede arbejde, viser nærmeste fallback og abonnerer på det kastede Promise. Når det løser sig, kører React komponenten igen fra toppen.

Promise-et skal være stabilt mellem disse kørsler. Et nyt Promise ved hver kørsel er en uendelig suspension. Dette er den mest almindelige måde at ødelægge use() på.

tsx

"use client";
import { use } from "react";
export default function Posts({ posts }) {
// suspenderer indtil promise løser sig
const allPosts = use(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Suspense, den statiske skal og HTML der ankommer i forkert rækkefølge

React sender den statiske skal straks. Det forsinkede stykke tilføjes i slutningen af dokumentet, ude af syne, og React streamer et lille inline <script> ved siden af det for at flytte indholdet til det rigtige sted.

tsx

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

Start tidligt, læs sent

tsx

import { Suspense } from "react";
import Posts from "@/app/ui/posts";
import Comments from "@/app/ui/comments";
export default function Page() {
// begge forespørgsler starter nu, parallelt
const posts = getPosts();
const comments = getComments();
return (
<>
<Suspense fallback={<div>Indlæser indlæg...</div>}>
<Posts posts={posts} />
</Suspense>
<Suspense fallback={<div>Indlæser kommentarer...</div>}>
<Comments comments={comments} />
</Suspense>
</>
);
}

Fælden ingen forventer: streaming bruger din HTTP-statuskode

Når en Suspense-fallback renderer og strømmen åbner sig, har serveren allerede forpligtet sig til 200 OK. Et notFound() der udløses inden i en streamet grænse kan ikke give en ægte 404.

tsx

export default async function PostPage({ params }) {
const { slug } = await params;
const exists = await checkSlugExists(slug); // hurtigt, inden strømmen åbner
if (!exists) notFound(); // ægte 404
return (
<Suspense fallback={<PostSkeleton />}>
<PostContent slug={slug} />
</Suspense>
);
}

Hvad streaming gør ved Core Web Vitals

  • TTFB falder til omtrent rendertiden for layoutet.
  • LCP kan forværres, ikke forbedres. Hold LCP-elementer i den statiske skal.
  • CLS er dit ansvar.
  • INP forbedres takket være selektiv hydration.

Faldgruber og bedste praksis

  • Opret aldrig Promise under rendering af en Client Component.
  • Suspense fanger venten, ikke fejl. Par hver grænse med en Error Boundary.
  • Dimensionér grænsen som en person ville gøre.
  • Bots modtager ikke strømmen.
  • En Promise-prop krydser netværket.

Konklusion

I stedet for en sekvens — forespørgsel, venten, rendering, forespørgsel, venten, rendering — får du parallelle strømme: alt starter på én gang, og hver del af siden vises så snart dens egne data ankommer.