Next.js
7. Streaming and Suspense
By default, a Server Component must finish executing completely before sending down HTML, even if parts of the data are ready much earlier. Isolating slow parts into async child components and wrapping them in `<Suspense>` enables Next.js to stream HTML in chunks.
On this page
A page often pulls from multiple data sources running at different speeds (fast data: core metadata, slow data: heavy reviews or analytics). When fetching data sequentially with await inside a single top-level component, users are forced to wait for the slowest dependency to complete before seeing anything on screen, even the content that was ready almost immediately.
1. The problem: sequential await forces the entire page to block on the slowest task
tsx
async function ProductPage({ params }) {
const product = await getProduct(params.id); // 50ms
const reviews = await getReviews(params.id); // 3000ms
return <div>{/* render both */}</div>;
}
- The entire component must finish executing (all
awaitexpressions) before streaming any HTML down. Even thoughproductresolved in 50ms, the user waits the full 3050ms before seeing any meaningful content.
2. Solution: extract async child components and wrap in <Suspense>
tsx
async function ProductPage({ params }) {
const product = await getProduct(params.id); // regular fast await
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
<Suspense fallback="{<p">Loading reviews...</p>}>
<Reviews productId="{params.id}"/> {/* NOT awaited in the parent component */}
</Suspense>
</div>
);
}
async function Reviews({ productId }) {
const reviews = await getReviews(productId); // 3000ms, isolated inside the child component
return <div>{/* render reviews */}</div>;
}
- Next.js streams the initial HTML shell for
ProductPage(name, price, and thefallbackUI) immediately after 50ms without waiting forReviews. OnceReviewscompletes, Next.js streams the resolved HTML chunk over the same open HTTP connection (Transfer-Encoding: chunked), and the client-side runtime automatically patches it into the fallback's DOM position. - This applies the exact same conceptual pattern as Node.js core Streams (
fs.createReadStream): emit chunks as soon as they are available, rather than buffering the entire payload in memory likefs.readFile().
3. <Suspense> boundaries treat children as a single unit
- If you bundle multiple slow components (reviews: 3s, related products: 2s, FAQ: 1s) inside a single shared
<Suspense>boundary, the user must wait until the slowest component (3s) finishes before all three are revealed simultaneously. - To display each section as soon as it resolves (independently of the others), wrap each component in its own discrete
<Suspense>boundary:
tsx
<Suspense fallback="{<p">Loading reviews...</p>}>
<Reviews productId="{id}"/>
</Suspense>
<Suspense fallback="{<p">Loading related products...</p>}>
<RelatedProducts productId="{id}"/>
</Suspense>
<Suspense fallback="{<p">Loading FAQs...</p>}>
<FAQ productId="{id}"/>
</Suspense>
- Result: the FAQ (1s) paints first, followed by related products (2s), and finally the reviews (3s).
